/** * This file is part of Obsidian, licensed under the MIT License (MIT). * * Copyright (c) 2013-2014 ObsidianBox <http://obsidianbox.org/> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.obsidianbox.obsidian.message; import java.lang.reflect.Field; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import com.google.common.collect.Maps; import cpw.mods.fml.common.network.FMLEmbeddedChannel; import cpw.mods.fml.common.network.FMLIndexedMessageToMessageCodec; import cpw.mods.fml.common.network.FMLOutboundHandler; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import gnu.trove.map.hash.TByteObjectHashMap; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.network.INetHandler; import net.minecraft.network.NetHandlerPlayServer; import org.obsidianbox.magma.Game; import org.obsidianbox.magma.addon.Addon; import org.obsidianbox.magma.message.Message; import org.obsidianbox.magma.message.MessageHandler; import org.obsidianbox.magma.message.MessagePipeline; @ChannelHandler.Sharable public class CommonMessagePipeline extends FMLIndexedMessageToMessageCodec<Message> implements MessagePipeline { private static Field discriminatorsField; private final Game game; private final EnumMap<Side, FMLEmbeddedChannel> channels = Maps.newEnumMap(Side.class); private final Map<Class<? extends Message>, Class<? extends MessageHandler<? extends Message>>> handlers = new HashMap<>(); private transient boolean locked = false; static { try { discriminatorsField = FMLIndexedMessageToMessageCodec.class.getDeclaredField("discriminators"); } catch (NoSuchFieldException e) { e.printStackTrace(); } } public CommonMessagePipeline(Game game) { this.game = game; } @Override public void encodeInto(ChannelHandlerContext ctx, Message message, ByteBuf target) throws Exception { // Allow addon API to write data message.encode(game, target); } @Override @SuppressWarnings({"unchecked", "rawtypes"}) public void decodeInto(ChannelHandlerContext ctx, ByteBuf source, Message message) { // Allow the addon API to decode it into the message's fields try { message.decode(game, source); } catch (Exception e) { game.getLogger().fatal("Exception caught decoding message!", e); return; } final Class<? extends MessageHandler> handlerClazz = handlers.get(message.getClass()); if (handlerClazz == null) { return; } MessageHandler<Message> handler; try { handler = handlerClazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { game.getLogger().error("Failed to handle message [" + message + "], does class [" + handlerClazz.getSimpleName() + "] have an empty constructor?"); return; } if (handler == null) { return; } Message toSendBack; // Handle the message switch (game.getSide()) { case CLIENT: toSendBack = handler.handle(game, getClientPlayer(), message); if (toSendBack != null) { sendToServer(toSendBack); } break; case SERVER: INetHandler net = ctx.channel().attr(NetworkRegistry.NET_HANDLER).get(); toSendBack = handler.handle(game, ((NetHandlerPlayServer) net).playerEntity, message); if (toSendBack != null) { sendTo(toSendBack, ((NetHandlerPlayServer) net).playerEntity); } break; default: } } @SideOnly(Side.CLIENT) private EntityPlayer getClientPlayer() { return Minecraft.getMinecraft().thePlayer; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { game.getLogger().fatal("Exception caught in pipeline!", cause); ctx.fireExceptionCaught(cause); } @Override @SuppressWarnings("unchecked") public <T extends Message, U extends MessageHandler<T>> void register(Addon addon, Class<T> message, Class<U> handler) { if (locked) { throw new IllegalStateException(addon.getDescription().getName() + " attempted to register a message after INITIALIZE phase! This is NOT ALLOWED."); } final TByteObjectHashMap<Class<? extends Message>> discriminators; try { discriminatorsField.setAccessible(true); discriminators = (TByteObjectHashMap<Class<? extends Message>>) discriminatorsField.get(this); discriminatorsField.setAccessible(false); } catch (IllegalAccessException e) { game.getLogger().info("Encountered fatal exception when " + addon.getDescription().getName() + " attempted to register [" + message.getSimpleName() + "]", e); return; } if (discriminators.containsValue(message)) { game.getLogger().warn(addon.getDescription().getName() + " attempted to register [" + message + "] twice!"); return; } addDiscriminator(discriminators.size() == 0 ? 0 : discriminators.size() + 1, message); if (handler != null) { handlers.put(message, handler); game.getLogger().info(addon.getDescription().getName() + " has registered message [" + message.getSimpleName() + "] with handler [" + handler.getSimpleName() + "] in the pipeline"); } else { game.getLogger().info(addon.getDescription().getName() + " has registered message [" + message.getSimpleName() + "] with no handler in the pipeline"); } } @Override public void sendToAll(Message message) { this.channels.get(Side.SERVER).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.ALL); this.channels.get(Side.SERVER).writeAndFlush(message); } @Override public void sendTo(Message message, EntityPlayer 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); } @Override public void sendToAllAround(Message 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); } @Override public void sendToDimension(Message 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); } @Override public void sendToServer(Message message) { this.channels.get(Side.CLIENT).attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.TOSERVER); this.channels.get(Side.CLIENT).writeAndFlush(message); } public void lockPipeline() { channels.putAll(NetworkRegistry.INSTANCE.newChannel(Game.MOD_ID.toUpperCase(), this)); locked = true; } }