package com.gmail.woodyc40.common.protocol.impl;
import com.gmail.woodyc40.common.protocol.*;
import com.gmail.woodyc40.common.protocol.events.PacketReceivedEvent;
import com.gmail.woodyc40.common.protocol.events.PacketSentEvent;
import com.gmail.woodyc40.common.protocol.events.ProtocolEvent;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import net.tridentsdk.Trident;
import net.tridentsdk.docs.InternalUseOnly;
import net.tridentsdk.entity.living.Player;
import net.tridentsdk.event.Listener;
import net.tridentsdk.event.player.PlayerDisconnectEvent;
import net.tridentsdk.event.player.PlayerJoinEvent;
import net.tridentsdk.factory.Factories;
import net.tridentsdk.meta.MessageBuilder;
import net.tridentsdk.plugin.TridentPluginHandler;
import net.tridentsdk.reflect.FastField;
import net.tridentsdk.server.TridentServer;
import net.tridentsdk.server.netty.ClientConnection;
import net.tridentsdk.server.netty.packet.PacketDirection;
import net.tridentsdk.server.netty.protocol.Protocol;
import net.tridentsdk.server.player.TridentPlayer;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.StampedLock;
/**
* Implements {@link com.gmail.woodyc40.common.protocol.ProtocolManagement}
*
* <p>To obtain the instance of this manager, use this code:
* <br/>
* {@code ProtocolManagement management = ProtocolManagement.manager();}</p>
*
* <p>All methods in this class are thread safe</p>
*
* @author Pierre C
*/
@ThreadSafe
public final class ManagementImpl implements ProtocolManagement, Listener {
/** The channel of a player's connection to the server */
private static final FastField CHANNEL_FIELD = Factories.reflect().field(ClientConnection.class, "channel");
/** The map of protocol listeners */
@GuardedBy("lock")
private final Map<Class<? extends ProtocolEvent>, List<ProtocolListener<?>>> listeners = Maps.newHashMap();
/** The lock used to guard the listeners in order to ensure thread safety */
private final StampedLock lock = new StampedLock();
/** The channels associated with the players */
private final Map<Player, Channel> channelMap = new ConcurrentHashMap<>();
// The listener map MUST use a lock
// The operations upon the map, in which a cache like system is used
// requires that the entire operation completes before the next begins.
// Unfortunately, when using a non-blocking, or concurrent solution,
// entries may become "leaked" when one write creates a new list and
// does not place the new list in before another write occurs, in
// which case, the first is lost.
// To put this into perspective:
//
// T1 -> Get EVENT = null -> Create new list and insert new listener -> reinsert new list
// T2 ------------------> Get EVENT = null -> Create new list and insert new listener -> reinsert new list
//
// The listener placed in T1 was subsequently removed by T2 as the key
// was assigned to a new instance of a list.
// It is therefore necessary to lock the entire operation between
// obtaining the event, and placing the list back into the listener map.
/**
* Do not instantiate this class, you will get an error
*
* <p>To obtain the instance of this manager, use this code:
* <br/>
* {@code ProtocolManagement management = ProtocolManagement.manager();}</p>
*/
@InternalUseOnly public ManagementImpl() {
Class<?> caller = Trident.findCaller(3);
Class<?> caller1 = Trident.findCaller(6);
// Ensure it isn't the real thing or the plugin manager
if (!caller.equals(ProtocolManagement.class)) {
if (!caller1.equals(TridentPluginHandler.class)) {
throw new UnsupportedOperationException("Use ProtocolManagement.manager() instead");
}
}
new Thread(() -> {
while (true) {
Packet packet = createPacket(0x02, true);
Object pos = null;
try {
pos = Enum.valueOf((Class<Enum>)
Class.forName("net.tridentsdk.server.packets.play.out.PacketPlayOutChat$ChatPosition"),
"CHAT");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
packet.write(new MessageBuilder("Hello World").build().asJson());
packet.write(pos);
final Packet finalPacket = packet;
send(pipe -> {
finalPacket.encode(pipe);
return TridentPlayer.players();
});
}
}).start();
}
@Override public <T extends ProtocolEvent> void intercept(ProtocolListener<T> listener) {
Class<T> eventType = (Class<T>) new TypeToken<T>(listener.getClass()) {}.getRawType();
long stamp = lock.writeLock();
try {
List<ProtocolListener<?>> listenerQueue = listeners.get(eventType);
if (listenerQueue == null) {
listenerQueue = Lists.newArrayList();
}
listenerQueue.add(listener);
listeners.put(eventType, listenerQueue);
} finally {
lock.unlockWrite(stamp);
}
}
@Override public <T extends ProtocolEvent> T transmit(T event, ProtocolAction action) {
Class<T> eventType = (Class<T>) new TypeToken<T>(event.getClass()) {}.getRawType();
long stamp = lock.readLock();
List<ProtocolListener<?>> listenerQueue;
try {
listenerQueue = listeners.get(eventType);
} finally {
lock.unlockRead(stamp);
}
if (listenerQueue == null) {
return event;
}
if (action == ProtocolAction.NO_OP) {
for (ProtocolListener<?> listener : listenerQueue) {
((ProtocolListener<T>) listener).intercept(event);
}
} else {
send(action);
for (ProtocolListener<?> listener : listenerQueue) {
((ProtocolListener<T>) listener).intercept(event);
}
}
return event;
}
@Override public <T extends ProtocolEvent> void remove(ProtocolListener<T> listener) {
Class<T> eventType = (Class<T>) new TypeToken<T>(listener.getClass()) {}.getRawType();
long stamp = lock.writeLock();
try {
List<ProtocolListener<?>> listenerQueue = listeners.get(eventType);
if (listenerQueue == null) {
throw new IllegalArgumentException("No such listener: " + listener.getClass());
}
if (!listenerQueue.remove(listener)) {
throw new IllegalArgumentException("No such listener: " + listener.getClass());
}
listeners.put(eventType, listenerQueue);
} finally {
lock.unlockWrite(stamp);
}
}
@Override public void send(ProtocolAction action) {
ByteBuf byteBuf = Unpooled.directBuffer();
ByteAppender appender = ByteAppender.wrap(byteBuf);
Collection<Player> targets = action.handle(appender);
if (targets != null) {
for (Player target : targets) {
if (target == null) {
throw new IllegalArgumentException("Cannot process null player");
}
Channel channel = channelMap.get(target);
if (channel != null) {
appender.writeTo(channel);
} else {
appender.writeTo(inject(target));
}
}
} else {
throw new IllegalArgumentException("Did you mean new Player[0]?");
}
}
@Override public void send(ByteAppender appender, Collection<Player> players) {
for (Player target : players) {
Channel channel = channelMap.get(target);
if (channel != null) {
appender.writeTo(channel);
} else {
appender.writeTo(inject(target));
}
}
}
@Override public void sendAll(ProtocolAction action) {
ByteBuf byteBuf = Unpooled.directBuffer();
ByteAppender appender = ByteAppender.wrap(byteBuf);
action.handle(appender);
send(appender, TridentPlayer.players());
}
@Override public Packet createPacket(int id, boolean out) {
return new PacketImpl(TridentServer
.instance()
.protocol()
.getPacket(id, Protocol.ClientStage.PLAY, out ? PacketDirection.OUT : PacketDirection.IN));
}
private Channel inject(Player player) {
TridentPlayer tridentPlayer = (TridentPlayer) player;
ClientConnection connection = tridentPlayer.connection();
// This only works because it's protected ;)
Channel channel = CHANNEL_FIELD.get(connection);
channel.pipeline().addBefore("PacketHandler#0", tridentPlayer.uniqueId().toString() + " interceptor", new ChannelHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
PacketReceivedEvent event =
transmit(new PacketReceivedEvent(PacketImpl.in(msg), tridentPlayer), ProtocolAction.NO_OP);
if (!event.ignored()) {
super.channelRead(ctx, msg);
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
PacketSentEvent event =
transmit(new PacketSentEvent(PacketImpl.out(msg), tridentPlayer), ProtocolAction.NO_OP);
if (!event.ignored()) {
super.write(ctx, msg, promise);
}
}
});
channelMap.put(player, channel);
return channel;
}
public void playerJoin(PlayerJoinEvent event) {
inject(event.player());
}
public void playerLeave(PlayerDisconnectEvent event) {
Channel channel = channelMap.get(event.player());
if (channel != null) {
channel.pipeline().remove(event.player().uniqueId().toString() + " interceptor");
}
}
}