Bukkitで送受信されているパケットにプラグインで割り込む

## 概要
本解説は1.7.x系を前提としています。
Bukkitで送受信されているパケットにプラグインで割り込み、改ざんします。
このプラグインにはBukkitAPIの他にnet.minecraftパッケージを必要とするので、
CraftBukkitをビルドパスに入れてください。
mavenを使用している場合は下記のように書き換えてください

```xml:pom.xml
<dependencies>
    <dependency>
        <groupId>org.bukkit</groupId>
        <artifactId>craftbukkit</artifactId>
        <version>1.7.2-R0.4-SNAPSHOT</version>
    </dependency>
</dependencies>
```

## 割り込み
Nettyを使用してpacket_handlerに割り込みます。
それにあたって3つのクラスを作成します。

```java:NettyInjecter.java
public class NettyInjector implements Listener {

    private Plugin plugin;

    HashMap<PlayerChannelHandler, Player> connection = new HashMap<>();

    private Class EntityPlayer;
    private Field EntityPlayer_playerConnection;

    private Class PlayerConnection;
    private Field PlayerConnection_networkManager;

    private Class NetworkManager;
    private Field NetworkManager_K;
    private Field NetworkManager_M;

    public NettyInjector(Plugin plugin){
        this.plugin = plugin;
        Bukkit.getServer().getPluginManager().registerEvents(this, plugin);

        try {

            EntityPlayer = Reflection.getCraftClass("EntityPlayer");
            EntityPlayer_playerConnection = Reflection.getField(EntityPlayer, "playerConnection");

            PlayerConnection = Reflection.getCraftClass("PlayerConnection");
            PlayerConnection_networkManager = Reflection.getField(PlayerConnection, "networkManager");

            NetworkManager = Reflection.getCraftClass("NetworkManager");
            NetworkManager_K = Reflection.getField(NetworkManager, "k");
            NetworkManager_M = Reflection.getField(NetworkManager, "m");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void inject(Player player) throws Exception{
        Object entityPlayer = Reflection.getEntityPlayer(player);
        Object networkManager = getNetworkManager(entityPlayer);
        Channel channel = getChannel(networkManager);
        PlayerChannelHandler pch = new PlayerChannelHandler();
        if(channel.pipeline().get(PlayerChannelHandler.class) == null){
            channel.pipeline().addBefore("packet_handler", "YourPluginName", pch);
            connection.put(pch, player);
        }
    }

    public void remove(final Player player) throws Exception{
        Object entityPlayer = Reflection.getEntityPlayer(player);
        Object networkManager = getNetworkManager(entityPlayer);
        final Channel channel = getChannel(networkManager);
        if(channel.pipeline().get(PlayerChannelHandler.class) != null){
            channel.pipeline().remove(PlayerChannelHandler.class);
            for (Map.Entry<PlayerChannelHandler, Player> es : connection.entrySet()) {
                if (es.getValue().equals(player)) {
                    connection.remove(es.getKey());
                }
            }
        }
    }

    @EventHandler
    public void onPluginDisable(PluginDisableEvent e) throws Exception{
        if(e.getPlugin().equals(plugin)){
            for(Player player: Bukkit.getOnlinePlayers()) {
                remove(player);
            }
        }
    }

    private Object getNetworkManager(Object entityPlayer) {
        Object pc = Reflection.getFieldValue(EntityPlayer_playerConnection, entityPlayer);
        return Reflection.getFieldValue(PlayerConnection_networkManager, pc);
    }

    private Channel getChannel(Object networkManager) {
        Channel ch = null;
        try {
            ch = Reflection.getFieldValue(NetworkManager_K, networkManager);
        } catch (Exception e) {
            ch = Reflection.getFieldValue(NetworkManager_M, networkManager);
        }
        return ch;
    }

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent e) throws Exception{
        inject(e.getPlayer());
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent e) throws Exception{
        remove(e.getPlayer());
    }

    @EventHandler
    public void onPluginEnable(PluginEnableEvent e) throws Exception{
        if(e.getPlugin().equals(plugin)){
            for(Player player: Bukkit.getOnlinePlayers()){
                inject(player);
            }
        }
    }
}
```

```java:Reflection.java
public class Reflection {

    public static String getPackageName(){
        String packagename = "net.minecraft.server."+getCraftBukkitVersion();
        return packagename;
    }

    public static String getCraftBukkitVersion(){
        String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
        return version;
    }

    public static Class<?> getCraftClass(String s) throws Exception {
        Class<?> craftclass = Class.forName(getPackageName()+"."+s);
        return craftclass;
    }

    public static Object getEntityPlayer(Player p) throws Exception{
        Method getHandle = p.getClass().getMethod("getHandle");
        return getHandle.invoke(p);
    }

    public static Object getFieldValue(Object instance, String fieldName) throws Exception {
        Field field = instance.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(instance);
    }

    @SuppressWarnings("unchecked")
    public static  <T> T getFieldValue(Field field, Object obj) {
        try {
            return (T) field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static Field getField(Class<?> clazz, String fieldName) throws Exception {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field;
    }

    public static void setFieldValue(Field field, Object obj, Object value) {
        try {
            field.set(obj, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

```java:PlayerChannelHandler.java
public class PlayerChannelHandler extends ChannelDuplexHandler {

    /**
     * クライアントからパケットを受信したとき
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg.getClass().getSimpleName().equalsIgnoreCase("PacketPlayInTabComplete")){
           String s = (String)Reflection.getFieldValue(msg, "a");
            if(!(s.startsWith("/ver") || s.startsWith("/version"))){
                super.channelRead(ctx, msg);
            }
        }else{
            super.channelRead(ctx, msg);
        }
    }

    /**
     * サーバーからクライアントへパケットを送信するとき
     * @param ctx
     * @param msg
     * @param promise
     * @throws Exception
     */
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        super.write(ctx, msg, promise);
    }
}
```

3つのクラスを作成し終わったら、最後にonEnableに下記のように記述します

```java:onEnable
@Override
public void onEnable() {
    new NettyInjector(this);
}
```

この記事の例では**PacketPlayInTabComplete**というタブ補完を行うときにサーバーに送られるパケットが**"/ver"**または、**"/version"**だった場合パケットを破棄するという処理を行っています。

どのパケットが何をしているかは下記をURL見てください。
[https://github.com/Bukkit/mc-dev/tree/master/net/minecraft/server](https://github.com/Bukkit/mc-dev/tree/master/net/minecraft/server)
[http://wiki.vg/Protocol](http://wiki.vg/Protocol)

下記の画像の状態でTABキーを押してもプラグインリストが出なければ成功です。
![alt](http://pic.masakoma.net/image/45f27e0da1ff98deed5e15aa35b793c8.png)

## 参考
[https://github.com/Bukkit/mc-dev/blob/master/net/minecraft/server/PacketPlayInTabComplete.java](https://github.com/Bukkit/mc-dev/blob/master/net/minecraft/server/PacketPlayInTabComplete.java)
[http://wiki.vg/Protocol#Tab-Complete](http://wiki.vg/Protocol#Tab-Complete)