Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mod communication system #14

Merged
merged 1 commit into from
May 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.gotti.wurmunlimited.modcomm;

import com.wurmonline.communication.SocketConnection;
import com.wurmonline.server.players.Player;

import java.nio.ByteBuffer;

/**
* Channel object, created by calling {@link ModComm#registerChannel}
*/
public class Channel {
final int id;
final IChannelListener listener;
final String name;

Channel(int id, String name, IChannelListener listener) {
this.id = id;
this.name = name;
this.listener = listener;
}

/**
* Send message to a player on this channel. Channel must be active for that player.
*
* @param player player object
* @param message contents of the message
*/
public void sendMessage(Player player, ByteBuffer message) {
if (!isActiveForPlayer(player))
throw new RuntimeException(String.format("Channel %s is not active for player %s", name, player.getName()));
try {
SocketConnection conn = player.getCommunicator().getConnection();
ByteBuffer buff = conn.getBuffer();
buff.put(ModCommConstants.CMD_MODCOMM);
buff.put(ModCommConstants.PACKET_MESSAGE);
buff.putInt(id);
buff.put(message);
buff.put(message);
conn.flush();
} catch (Exception e) {
ModComm.logException(String.format("Error sending packet on channel %s to player %s", name, player.getName()), e);
}
}

/**
* Check if a channel is active for a specific player.
*
* @param player player object
* @return true if the channel is active
*/
public boolean isActiveForPlayer(Player player) {
PlayerModConnection conn = ModComm.getPlayerConnection(player);
return conn.isActive() && conn.getChannels().contains(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.gotti.wurmunlimited.modcomm;

import com.wurmonline.server.players.Player;

import java.nio.ByteBuffer;

/**
* Listener for mod channels, implement in a class and register with {@link ModComm#registerChannel}
*/
public interface IChannelListener {
/**
* Handle a message from a player
*
* @param player player object
* @param message message contents
*/
default void handleMessage(Player player, ByteBuffer message) {
}

/**
* Called when a player is connected and this channel is activated
*
* @param player player object
*/
default void onPlayerConnected(Player player) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package org.gotti.wurmunlimited.modcomm;

import com.wurmonline.server.players.Player;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import org.gotti.wurmunlimited.modloader.classhooks.HookManager;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ModComm {
static final HashMap<String, Channel> channels = new HashMap<>();
static final HashMap<Integer, Channel> idMap = new HashMap<>();

private static int nextChannelId = 1;

private static Field fPlayerConnection;

private static final Logger logger = Logger.getLogger("ModComm");

/**
* Register mod channel
*
* @param name Unique identifier of the channel
* @param listener Listener that will handle communication
* @return new channel object
*/
public static Channel registerChannel(String name, IChannelListener listener) {
if (channels.containsKey(name))
throw new RuntimeException(String.format("Channel %s already registered", name));
Channel ch = new Channel(nextChannelId++, name, listener);
idMap.put(ch.id, ch);
channels.put(name, ch);
return ch;
}

// === internal stuff ===

/**
* Get player connection state
*/
@SuppressWarnings("unchecked")
static PlayerModConnection getPlayerConnection(Player player) {
try {
return (PlayerModConnection) fPlayerConnection.get(player);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

/**
* Internal initialization, called from {@link org.gotti.wurmunlimited.modloader.ModLoader#loadModsFromModDir}
*/
public static void init() {
final ClassPool classPool = HookManager.getInstance().getClassPool();
try {
CtClass ctPlayer = classPool.getCtClass("com.wurmonline.server.players.Player");

CtField fConnection = new CtField(classPool.get("org.gotti.wurmunlimited.modcomm.PlayerModConnection"), "modConnection", ctPlayer);
fConnection.setModifiers(Modifier.PUBLIC);
ctPlayer.addField(fConnection, "new org.gotti.wurmunlimited.modcomm.PlayerModConnection()");

CtClass ctCommunicator = classPool.getCtClass("com.wurmonline.server.creatures.Communicator");
ctCommunicator.getMethod("reallyHandle", "(ILjava/nio/ByteBuffer;)V").instrument(new ExprEditor() {
private boolean first = true;

@Override
public void edit(MethodCall m) throws CannotCompileException {
if (m.getMethodName().equals("get") && first) {
m.replace("$_ = $proceed($$);" +
"if ($_ == " + ModCommConstants.CMD_MODCOMM + ") {" +
" org.gotti.wurmunlimited.modcomm.ModCommHandler.handlePacket(player, byteBuffer);" +
" return;" +
"}");
first = false;
}
}
});
} catch (NotFoundException | CannotCompileException e) {
throw new RuntimeException("Error initializing ModComm", e);
}
}

/**
* Internal late initialization, called from {@link org.gotti.wurmunlimited.modloader.ServerHook#fireOnServerStarted}
*/
public static void serverStarted() {
try {
fPlayerConnection = Player.class.getDeclaredField("modConnection");
} catch (NoSuchFieldException e) {
throw new RuntimeException("Error initializing ModComm", e);
}
}

/**
* Player connected handler, called from {@link org.gotti.wurmunlimited.modloader.ServerHook#fireOnPlayerLogin}
*/
public static void playerConnected(Player player) {
if (!channels.isEmpty())
player.getCommunicator().sendNormalServerMessage(ModCommConstants.BANNER);
}

// === Logging ===

static void logException(String msg, Throwable e) {
if (logger != null)
logger.log(Level.SEVERE, msg, e);
}

static void logWarning(String msg) {
if (logger != null)
logger.log(Level.WARNING, msg);
}

static void logInfo(String msg) {
if (logger != null)
logger.log(Level.INFO, msg);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.gotti.wurmunlimited.modcomm;

public class ModCommConstants {
/**
* Packet ID for all packets used by this system, should not collide with any packets used by WU (see {@link com.wurmonline.shared.constants.ProtoConstants})
*/
public static final byte CMD_MODCOMM = -100;

/**
* Marker that will be detected by the client to initiate the handshake process
*/
public static final String MARKER = "[ModCommV1]";

/**
* Human readable message that will be sent to connecting players
*/
public static final String BANNER = MARKER + " This is a modded server, additional features might be available if you install Ago's Client Mod Launcher (http://forum.wurmonline.com/index.php?/topic/134945-/)";

/**
* Version of the internal protocol
*/
public static final byte PROTO_VERSION = 1;

/**
* Packet types for the internal protocol
*/
public static final byte PACKET_MESSAGE = 1;
public static final byte PACKET_CHANNELS = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.gotti.wurmunlimited.modcomm;

import com.wurmonline.communication.SocketConnection;
import com.wurmonline.server.players.Player;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashSet;

public class ModCommHandler {
public static void handlePacket(Player player, ByteBuffer msg) {
try {
byte type = msg.get();
switch (type) {
case ModCommConstants.PACKET_MESSAGE:
handlePacketMessage(player, msg);
break;
case ModCommConstants.PACKET_CHANNELS:
handlePacketChannels(player, msg);
break;
default:
ModComm.logWarning(String.format("Unknown packet from player %s (%d)", player, type));
}
} catch (Exception e) {
ModComm.logException(String.format("Error handling packet from player %s", player.getName()), e);
}
}

private static void handlePacketChannels(Player player, ByteBuffer msg) throws IOException {
PacketReader reader = new PacketReader(msg);
HashSet<Channel> toActivate = new HashSet<>();

byte version = reader.readByte();
int n = reader.readInt();

ModComm.logInfo(String.format("Received client handshake from %s, %d channels, protocol version %d", player.getName(), n, version));

while (n-- > 0) {
String channel = reader.readUTF();
if (ModComm.channels.containsKey(channel))
toActivate.add(ModComm.channels.get(channel));
}

ModComm.logInfo(String.format("Activating %d channels for player %s", toActivate.size(), player.getName()));

ModComm.getPlayerConnection(player).activate(version, toActivate);

try (PacketWriter writer = new PacketWriter()) {
writer.writeByte(ModCommConstants.CMD_MODCOMM);
writer.writeByte(ModCommConstants.PACKET_CHANNELS);
writer.writeByte(ModCommConstants.PROTO_VERSION);
writer.writeInt(toActivate.size());
for (Channel channel : toActivate) {
writer.writeInt(channel.id);
writer.writeUTF(channel.name);
}
SocketConnection conn = player.getCommunicator().getConnection();
ByteBuffer buff = conn.getBuffer();
buff.put(writer.getBytes());
conn.flush();
}

for (Channel channel : toActivate) {
try {
channel.listener.onPlayerConnected(player);
} catch (Exception e) {
ModComm.logException(String.format("Error in channel %s onPlayerConnected", channel.name), e);
}
}
}

private static void handlePacketMessage(Player player, ByteBuffer msg) {
int id = msg.getInt();
if (!ModComm.idMap.containsKey(id)) {
ModComm.logWarning(String.format("Message on unregistered channel %d from player %s", id, player.getName()));
return;
}
Channel ch = ModComm.idMap.get(id);
if (!ch.isActiveForPlayer(player)) {
ModComm.logWarning(String.format("Message on inactive channel %s from player %s", ch.name, player.getName()));
return;
}
try {
ch.listener.handleMessage(player, msg.slice());
} catch (Exception e) {
ModComm.logException(String.format("Error in channel handler %s for player %s", ch.name, player.getName()), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.gotti.wurmunlimited.modcomm;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

/**
* Convenience class for reading packets, see {@link DataInputStream docs for the various reading methods}
*/
public class PacketReader extends DataInputStream {
private static class ByteBufferBackedInputStream extends InputStream {
private final ByteBuffer buf;

private ByteBufferBackedInputStream(ByteBuffer buf) {
this.buf = buf;
}

public int read() throws IOException {
if (buf.hasRemaining()) {
return buf.get() & 0xFF;
} else {
return -1;
}
}

public int read(byte[] bytes, int off, int len) throws IOException {
if (buf.hasRemaining()) {
len = Math.min(len, buf.remaining());
buf.get(bytes, off, len);
return len;
} else {
return -1;
}
}
}

public PacketReader(ByteBuffer buffer) {
super(new ByteBufferBackedInputStream(buffer));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.gotti.wurmunlimited.modcomm;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.ByteBuffer;

/**
* Convenience class for writing packets, see {@link DataOutputStream docs for the various writing methods}
*/
public class PacketWriter extends DataOutputStream {
private final ByteArrayOutputStream buffer;

public PacketWriter() {
super(new ByteArrayOutputStream());
buffer = (ByteArrayOutputStream) out;
}

public ByteBuffer getBytes() {
return ByteBuffer.wrap(buffer.toByteArray(), 0, buffer.size());
}
}