この記事は"Minecraft Forge Universal 10.12.0.xxx~"を前提MODとしています。 |
目次
パケットについて
1.7.2でパケット関連のシステムが大幅に変更され、nettyと呼ばれるオープンプロジェクトのシステムを利用したHandshake方式になりました。
Sirgingalot氏によるチュートリアルが良いものなので、それの和訳ベースで幾つか注釈をつけて解説したいと思います。
はじめに
これは、FMLで用意されているSimpleChannelHandlerの代替手段です。(注:というのも、SimpleChannelHandler は現状上手く動作しない)
この手法では、自動でパケットの識別子が生成され、クライアント、サーバーの両方の処理を1つのパケットクラス内で処理することが出来ます。
パケット構造の例
今回は、以下に記すAbstract Classをパケットをすべてのパケットで継承し、その上で、必要な拡張を行う形を取ります。
サイド別のパケット受け取り処理はそれぞれ'handle'メソッドで行うことが出来ます。
注意:このクラスを継承した子クラスには必ず、空のコンストラクタを設定して下さい。
(注:つまり、引数を持つコンストラクタを作ったらな、必ず、引数なしのものも作る必要が有るということです。)
(注:あとで、パケットクラスの例を載せますが、引数ありのコンストラクタを用意するのが色々便利なので、空のコンストラクタを付けることを忘れないようにして下さい。)
AbstractPacket Class
AbstractPacket.java
package あなたのMODのパッケージ; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import net.minecraft.entity.player.EntityPlayer; /** * AbstractPacket クラス PacketPipelineで扱うパケットはすべてこのクラスを継承する. * @author sirgingalot */ public abstract class AbstractPacket { /** * パケットのデータをByteBufに変換する. 複雑なデータ(NBTとか)はハンドラーが用意されている ( @link{cpw.mods.fml.common.network.ByteBuffUtils}を見てくれ) * * @param ctx channel context(和訳注:殆ど使わない) * @param buffer 変換するByteBufの変数。これにwriteしていく。 */ public abstract void encodeInto(ChannelHandlerContext ctx, ByteBuf buffer); /** * ByteBufからデータを取り出す. 複雑なデータはハンドラーが用意されている ( @link{cpw.mods.fml.common.network.ByteBuffUtils}を見てくれ) * * @param ctx channel context(普段は使わない) * @param buffer データを取り出すByteBuf。これからreadしていく。 */ public abstract void decodeInto(ChannelHandlerContext ctx, ByteBuf buffer); /** * クライアント側でパケットを受け取った後処理するメソッド。decodeIntoでByteBufが読み出されたあとに実行される。 * * @param player the player reference */ public abstract void handleClientSide(EntityPlayer player); /** * サーバー側でパケットを受け取った後処理するメソッド。decodeIntoでByteBufが読み出されたあとに実行される。 * * @param player the player reference */ public abstract void handleServerSide(EntityPlayer player); }
パケットハンドラー
パケットを処理するメインのクラスです。 インラインエンコード/デコード処理を可能とする、パケットの識別子は自動的に生成されます。 また、再度別処理をパケットクラスで行うことも可能となります。 注:チャンネル名称を必ず変更して下さい。(現在"チャンネル名称"になっている)
PacketPipeline Class
package あなたのMODのパッケージ; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageCodec; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; import java.util.LinkedList; import java.util.List; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.network.INetHandler; import net.minecraft.network.NetHandlerPlayServer; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.network.FMLEmbeddedChannel; import cpw.mods.fml.common.network.FMLOutboundHandler; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.network.internal.FMLProxyPacket; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; /** * Packet pipeline クラス。パケットの登録と、パケットのSendToを行う。 * @author sirgingalot * some code from: cpw */ @ChannelHandler.Sharable public class PacketPipeline extends MessageToMessageCodec<FMLProxyPacket, AbstractPacket> { //こちらで使わないので、知る必要はないが、登録するChannelの変数。基本的に1MOD(1 packetPipelineインスタンス)に1Channel private EnumMap<Side, FMLEmbeddedChannel> channels; //こちらも使わない。Packetクラスの保存先。1Channelに付き256種類のPacketが登録できる。 private LinkedList<Class<? extends AbstractPacket>> packets = new LinkedList<Class<? extends AbstractPacket>>(); //使わない。PostInitされたかどうか。 private boolean isPostInitialised = false; /** * pipelineにpacketを登録するメソッド。識別子は自動でセットされる。256個までしか登録できない。 * * @param clazz the class to register * * @return 登録が成功したかどうか。256個以上の登録、同クラス登録、postInitialise内で登録するとfalseになる。 */ public boolean registerPacket(Class<? extends AbstractPacket> clazz) { if (this.packets.size() > 256) { //256個以上登録しようとした。 // You should log here!!(logを出力すべき。) return false; } if (this.packets.contains(clazz)) { //同じクラスを登録しようとした。 // You should log here!! return false; } if (this.isPostInitialised) { //postinit処理で登録しようとした。 // You should log here!! return false; } this.packets.add(clazz); return true; } // packetのエンコード処理。ここで、識別子が設定されている。 @Override protected void encode(ChannelHandlerContext ctx, AbstractPacket msg, List<Object> out) throws Exception { ByteBuf buffer = Unpooled.buffer(); Class<? extends AbstractPacket> clazz = msg.getClass(); if (!this.packets.contains(msg.getClass())) { throw new NullPointerException("No Packet Registered for: " + msg.getClass().getCanonicalName()); } byte discriminator = (byte) this.packets.indexOf(clazz); buffer.writeByte(discriminator); msg.encodeInto(ctx, buffer); FMLProxyPacket proxyPacket = new FMLProxyPacket(buffer.copy(), ctx.channel().attr(NetworkRegistry.FML_CHANNEL).get()); out.add(proxyPacket); } // packetのデコード処理。 @Override protected void decode(ChannelHandlerContext ctx, FMLProxyPacket msg, List<Object> out) throws Exception { ByteBuf payload = msg.payload(); byte discriminator = payload.readByte(); Class<? extends AbstractPacket> clazz = this.packets.get(discriminator); if (clazz == null) { throw new NullPointerException("No packet registered for discriminator: " + discriminator); } AbstractPacket pkt = clazz.newInstance(); pkt.decodeInto(ctx, payload.slice()); EntityPlayer player; switch (FMLCommonHandler.instance().getEffectiveSide()) { case CLIENT: player = this.getClientPlayer(); pkt.handleClientSide(player); break; case SERVER: INetHandler netHandler = ctx.channel().attr(NetworkRegistry.NET_HANDLER).get(); player = ((NetHandlerPlayServer) netHandler).playerEntity; pkt.handleServerSide(player); break; default: } out.add(pkt); } // 初期化メソッド。FMLInitializationEventで呼び出す。 public void initialise() { this.channels = NetworkRegistry.INSTANCE.newChannel("チャンネル名称", this); } // post初期化メソッド。FMLPostInitializationEventで呼び出す。 // packetの識別子がクライントとサーバーで同一なものか確認している。 public void postInitialise() { if (this.isPostInitialised) { return; } this.isPostInitialised = true; Collections.sort(this.packets, new Comparator<Class<? extends AbstractPacket>>() { @Override public int compare(Class<? extends AbstractPacket> clazz1, Class<? extends AbstractPacket> clazz2) { int com = String.CASE_INSENSITIVE_ORDER.compare(clazz1.getCanonicalName(), clazz2.getCanonicalName()); if (com == 0) { com = clazz1.getCanonicalName().compareTo(clazz2.getCanonicalName()); } return com; } }); } @SideOnly(Side.CLIENT) private EntityPlayer getClientPlayer() { return Minecraft.getMinecraft().thePlayer; } /** * プレイヤー全員にpacketを送る。 * <p/> * Adapted from CPW's code in cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper * * @param message The message to send */ public void sendToAll(AbstractPacket message) { this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.ALL); this.channels.get(Side.SERVER).writeAndFlush(message); } /** * あるプレイヤーにpacketを送る。 * <p/> * Adapted from CPW's code in cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper * * @param message The message to send * @param player The player to send it to */ public void sendTo(AbstractPacket message, EntityPlayerMP player) { this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.PLAYER); this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGETARGS).set(player); this.channels.get(Side.SERVER).writeAndFlush(message); } /** * ある一定範囲内にいるプレイヤーにpacketを送る。 * <p/> * Adapted from CPW's code in cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper * * @param message The message to send * @param point The {@link cpw.mods.fml.common.network.NetworkRegistry.TargetPoint} around which to send */ public void sendToAllAround(AbstractPacket message, NetworkRegistry.TargetPoint point) { this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.ALLAROUNDPOINT); this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGETARGS).set(point); this.channels.get(Side.SERVER).writeAndFlush(message); } /** * 指定ディメンションにいるプレイヤー全員にpacketを送る。 * <p/> * Adapted from CPW's code in cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper * * @param message The message to send * @param dimensionId The dimension id to target */ public void sendToDimension(AbstractPacket message, int dimensionId) { this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.DIMENSION); this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGETARGS).set(dimensionId); this.channels.get(Side.SERVER).writeAndFlush(message); } /** * サーバーにpacketを送る。 * <p/> * Adapted from CPW's code in cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper * * @param message The message to send */ public void sendToServer(AbstractPacket message) { this.channels.get(Side.CLIENT).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.TOSERVER); this.channels.get(Side.CLIENT).writeAndFlush(message); } }
Pipelineの登録
上のクラスを以下の方法でFMLに登録します。
@Modクラス内での記述
public static final PacketPipeline packetPipeline = new PacketPipeline(); @EventHandler public void initialise(FMLInitializationEvent evt) { packetPipeline.initialise(); //ここでパケットクラスの登録をする。 packetPipeline.registerPacket(KeyHandlingPacket.class); } @EventHandler public void postInitialise(FMLPostInitializationEvent evt) { packetPipeline.postInitialise(); }
パケットの登録
パケットの登録は、先ほどのpacketPipelineクラスのinitialise()メソッド内か、例のソースのようにinitialise()メソッド後に行って下さい。
パケットクラスの例
私が動作確認したパケットクラスを載せておきます。 また、Tinker's Constructの例は Tinker's Construct Packets で見ることが出来ます。
package ak.ChainDestruction; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import net.minecraft.entity.player.EntityPlayer; //クライアント側でキー入力によって変化したboolean変数をサーバー側に伝達するパケット。AbstractPacketを継承 public class KeyHandlingPacket extends AbstractPacket { //保持しておくboolean型変数 boolean toggle; boolean digUnderToggle; //引数を持つコンストラクタを追加する場合は、空のコンストラクタを用意してくれとのこと。 public KeyHandlingPacket() { } //パケット生成を簡略化するために、boolean型変数を引数に取るコンストラクタを追加。 public KeyHandlingPacket(boolean ver1, boolean ver2) { toggle = ver1; digUnderToggle = ver2; } @Override public void encodeInto(ChannelHandlerContext ctx, ByteBuf buffer) { //ByteBufに変数を代入。基本的にsetメソッドではなく、writeメソッドを使う。 buffer.writeBoolean(toggle); buffer.writeBoolean(digUnderToggle); } @Override public void decodeInto(ChannelHandlerContext ctx, ByteBuf buffer) { //ByteBufから変数を取得。こちらもgetメソッドではなく、readメソッドを使う。 toggle = buffer.readBoolean(); digUnderToggle = buffer.readBoolean(); } @Override public void handleClientSide(EntityPlayer player) { //今回はクライアントの情報をサーバーに送るので、こちらはなにもしない。 //NO OP } @Override public void handleServerSide(EntityPlayer player) { //代入したいクラスの変数に代入。Worldインスタンスはplayerから取得できる。 ChainDestruction.interactblockhook.toggle = toggle; ChainDestruction.interactblockhook.digUnderToggle = digUnderToggle; } }