(パケット継承しないでPacket250CustomPayloadを使おう) |
|||
1行目: | 1行目: | ||
+ | ''編注: このページの内容は近代的なバージョンでは適用できないため、他のページを参照してください。'' | ||
本体ver1.3以降より、通常のクライアント版でもサーバ版と同様の構成に変わりました。<br /> | 本体ver1.3以降より、通常のクライアント版でもサーバ版と同様の構成に変わりました。<br /> |
2023年11月15日 (水) 22:34時点における最新版
編注: このページの内容は近代的なバージョンでは適用できないため、他のページを参照してください。
本体ver1.3以降より、通常のクライアント版でもサーバ版と同様の構成に変わりました。
ここではmod作成時に注意すべき点を記述します。
本体プログラムの構成について[編集]
minecraft本体は、実際のワールド情報を保持するサーバと、それらの情報を受け取って
画面に表示するクライアントに分離されています。
よってクライアント側のワールドをいくら操作しても、サーバ側は変化しません。
あっという間に元通りになってしまいます。
クライアント側のプレイヤーの周りに経験値オーブを大量発生させても、次の瞬間には消えていることでしょう。
ではクライアント側ワールドはmodに不必要なのか、というとそうではありません。
ブロックやアイテム(経験値オーブ含む)、他のプレイヤーやmobなどの状態を取得することに使用します。
なぜクライアント側ワールドでそれを取得するのか?サーバ側ワールドで情報取得をすれば良いのではないか?
実は後者で情報取得すると、クライアント側ワールドに未だ表示されていない情報が取得できてしまいます。
こうなると、見えない経験値オーブを取得した音がなった!とかいきなりmobがワープしてきた!という怪奇現象につながりかねません。
現在のワールドの情報を取得するときはクライアント側、実際にワールドの情報を書き換える場合はサーバ側とおぼえましょう。
もう一つ重要な点として、サーバ側とクライアント側のワールドは別々のスレッドで動いていることが挙げられます。
SMP であれば、スレッドどころかプロセスが異なります。プロセスが動いているマシンごと異なっていることもあります。
ModLoaderのmodはクライアント側ワールドを担当するスレッドで動きます。サーバ側ワールドは別のスレッドが担当しています。
ですので、サーバ側ワールドへの変更を、直接modからやってしまうとマルチスレッドでの不具合が発生します。
ではどうするのか、以下次章。
サーバ側スレッド、もしくはサーバプロセスで動くために[編集]
mod はクライアントスレッドで動いています。サーバスレッド、もしくはサーバプロセスで動くためには何かしらきっかけ、イベント、メッセージが必要です。
以前、パケットクラスを継承してそれを投げれば、と書いたのですがそれはシングルプレイのみで有効な方法でした。
パケットクラスは厳重に管理されており、本体を直接改造する以外で新たなパケットを正当に登録することは至難の業です。
SMP も視野に入れる、本来の正当な形のパケットメッセージを投げるには、Packet250CustomPayload クラスを使います。
Packet250CustomPayload はビルトインのパケットクラスで、クライアント側からもサーバ側からも投げることができます。
ModLoader はこれを扱う方法を用意しています。それが ModLoader.registerPacketChannel メソッドです。
mod のインスタンスをチャネル名(15文字以下)とともに登録することで、そのチャネル名を持つ Packet250CustomPayload クラスのインスタンスが届いた時に
mod インスタンスの clientCustomPayload, serverCustomPayload メソッドを呼んでくれます。
サーバ側からクライアントへ投げた場合は前者が、クライアントから接続先サーバへ投げた場合は後者のメソッドが呼ばれます。
Packet250CustomPayload はチャネル名の他に 32kb までのバイト配列をもたせられますので、必要な情報をここに乗せて投げれば目的を達成できます。
シングルプレイ用modを作るときは[編集]
この章は無視してください!Packet250CustomPayload クラスを使いましょう!
前章でも記述したとおり、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 クラスで使用可能になります。
間違いでした。TcpConnection クラスでも MemoryConnection クラスでも、isRealPacket() の戻り値にかかわらずパケットは送信されます。
isRealPacket() で true を返した場合、そのパケットクラスでは containsSameEntityIDAs() にて同値のパケットかどうかを判定されます。
同値のパケットだと返した場合、後のパケットだけが送信され、前のパケットは破棄されます。
複数の同値のパケットを一つにまとめて投げるための機構と思われます。なので、自作パケットにはほとんど関係ありません。
isRealPacket() は常に false を返して OK です。
もちろんサーバ側の minecraft にも ModLoader、同一 mod が入っていないと
未知のパケットとしてドロップされてしまいますが。
SMP で使用を許可しない場合は isRealPacket で false を返せばシングルプレイ専用になります。
SMP 対応する場合はさらに別の方法になります。後日書きます。
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 番目にプレイヤーのインスタンスが入ります。