提供: Minecraft Modding Wiki
2014年5月13日 (火) 22:56時点におけるA.K. (トーク | 投稿記録)による版 (新規作成)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
移動先: 案内検索

この記事は"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;

   }

}