本体ver1.3以降より、通常のクライアント版でもサーバ版と同様の構成に変わりました。
ここではmod作成時に注意すべき点を記述します。
本体プログラムの構成について
minecraft本体は、実際のワールド情報を保持するサーバと、それらの情報を受け取って
画面に表示するクライアントに分離されています。
よってクライアント側のワールドをいくら操作しても、サーバ側は変化しません。
あっという間に元通りになってしまいます。
クライアント側のプレイヤーの周りに経験値オーブを大量発生させても、次の瞬間には消えていることでしょう。
ではクライアント側ワールドはmodに不必要なのか、というとそうではありません。
ブロックやアイテム(経験値オーブ含む)、他のプレイヤーやmobなどの状態を取得することに使用します。
なぜクライアント側ワールドでそれを取得するのか?サーバ側ワールドで情報取得をすれば良いのではないか?
実は後者で情報取得すると、クライアント側ワールドに未だ表示されていない情報が取得できてしまいます。
こうなると、見えない経験値オーブを取得した音がなった!とかいきなりmobがワープしてきた!という怪奇現象につながりかねません。
現在のワールドの情報を取得するときはクライアント側、実際にワールドの情報を書き換える場合はサーバ側とおぼえましょう。
もう一つ重要な点として、サーバ側とクライアント側のワールドは別々のスレッドで動いていることが挙げられます。
SMP であれば、スレッドどころかプロセスが異なります。プロセスが動いているマシンごと異なっていることもあります。
ModLoaderのmodはクライアント側ワールドを担当するスレッドで動きます。サーバ側ワールドは別のスレッドが担当しています。
ですので、サーバ側ワールドへの変更を、直接modからやってしまうとマルチスレッドでの不具合が発生します。
ではどうするのか、以下次章。
シングルプレイ用modを作るときは
前章でも記述したとおり、ModLoaderで動作するmodはクライアント側ワールド担当のスレッドで動きます。
サーバ側ワールドを改変するにはなんとかしてそれを担当するスレッドで動かなくてはなりません。
サーバ側ワールド担当スレッドでプログラムを動かすには、net.minecraft.src.Packet を継承したパケットを
クライアント側ワールドの送信キューに入れれば良いのです。
public static class Packet_mod_CutAll extends Packet { public int _x = 0; public int _y = 0; public int _z = 0; public int blockID = 0; public int metadata = 0; public String playerName = ""; public Packet_mod_CutAll(String playerName) { this.playerName = playerName; } @Override public void readPacketData(DataInputStream var1) throws IOException { this._x = var1.readInt(); this._y = var1.readInt(); this._z = var1.readInt(); this.blockID = var1.readInt(); this.metadata = var1.readInt(); this.playerName = var1.readUTF(); } @Override public void writePacketData(DataOutputStream var1) throws IOException { var1.writeInt(this._x); var1.writeInt(this._y); var1.writeInt(this._z); var1.writeInt(this.blockID); var1.writeInt(this.metadata); var1.writeUTF(this.playerName); } @Override public void processPacket(NetHandler nh) { // ここにサーバ側ワールドを改変するソースを書く。 } @Override public int getPacketSize() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); try { this.writePacketData(dos); } catch (IOException e) { } finally { try { dos.close(); } catch (IOException e) { } } return baos.toByteArray().length; } @Override public String toString() { return String.format("Packet_mod_DigAll (%d, %d, %d) => (blockid=%d, metadata=%d)" , this._x, this._y, this._z, this.blockID, this.metadata); } @Override public boolean isRealPacket() { return false; } @Override public boolean isWritePacket() { return false; } }
readPacketData, writePacketData を除けばほぼそのまま上記のソースは使えると思います。
isRealPacket で true を返すと、SMP で使用出来る mod になる、と思われます。
具体的には TcpConnection クラスで使用可能になります。
もちろんサーバ側の minecraft にも ModLoader、同一 mod が入っていないと
未知のパケットとしてドロップされてしまいますが。
SMP で使用を許可しない場合は isRealPacket で false を返せばシングルプレイ専用になります。
isWritePacket は false を返してください。これでサーバ側ワールド担当スレッドがこのパケットの processPacket メソッドを
呼んでくれるようになります。true だと通信用スレッドで呼ばれちゃう、んだと思う…。
このパケットをキューに入れるには以下のようにします。
Minecraft minecraft = ModLoader.getMinecraftInstance(); minecraft.getSendQueue().addToSendQueue( new Packet_mod_CutAll(minecraft.thePlayer.getEntityName()));
くれぐれもスレッドの違いに気をつけてください。
あ、サーバ側ワールドのインスタンスを取得する方法を書いてないや。
public static Object[] getServerWorldAndPlayer(String playerName) { final Minecraft minecraft = ModLoader.getMinecraftInstance(); World world = minecraft.theWorld; EntityPlayer player = minecraft.thePlayer; if (null == minecraft.getIntegratedServer()) { // ver1.3 以降はありえないはずだが…。クライアント状態のみ。 } else if (null == minecraft.getIntegratedServer().theWorldServer || minecraft.getIntegratedServer().theWorldServer.length <= 0) { // えー、これもないはず…。ワールドが無いとかありえん。 //ModLoader.getLogger().info("onTickInGame no WorldServers."); } else { // サーバワールド側のプレイヤーを取得。 player = minecraft.getIntegratedServer().getConfigurationManager().getPlayerForUsername(playerName); // クライアント世界側のエンティティに対応する、サーバ世界側のエンティティを取得。 world = minecraft.getIntegratedServer().worldServerForDimension(player.dimension); } return new Object[] { world, player }; }
Objectの配列の 0 番目にサーバ側ワールドのインスタンス、1 番目にプレイヤーのインスタンスが入ります。