Compare commits

...
48 Commits
Author SHA1 Message Date
Kristian S. Stangeland cd9fa1cdf6 Fixing documentation. 2012-09-18 17:20:48 +02:00
Kristian S. Stangeland ad6fbae5d6 Incrementing to version 1.1.0 2012-09-18 17:17:20 +02:00
Kristian S. Stangeland 8f9939f65c Added the ability to refresh the apperance of entities and players. 2012-09-18 16:30:46 +02:00
Kristian S. Stangeland 3783328cb1 Forgot to convert the types to NMS. 2012-09-18 02:16:11 +02:00
Kristian S. Stangeland 140edabb4e Fixed packet constructor and tons of other bugs. 2012-09-18 02:04:26 +02:00
Kristian S. Stangeland ffbaed283a Improved the API slightly. 2012-09-18 00:01:02 +02:00
Kristian S. Stangeland e5beca4ea5 And the PacketConstructor class itself. 2012-09-17 22:59:41 +02:00
Kristian S. Stangeland fb15ee1b22 Adding packet constructors.
This should simplify a lot of code.
2012-09-17 22:59:20 +02:00
Kristian S. Stangeland 7a60c5aac7 Made the server registry static. 2012-09-17 16:40:08 +02:00
Kristian S. Stangeland f74a6e9832 Made the packet ids static and thus usable in a switch statement. 2012-09-17 16:38:26 +02:00
Kristian S. Stangeland 381484fc5d Adding a simple packet registry. 2012-09-17 16:21:15 +02:00
Kristian S. Stangeland 455ffca63f Adding a entity structure modifier. 2012-09-17 01:34:10 +02:00
Kristian S. Stangeland 286e1159e9 Made certain classes private. No need to expose them to the API. 2012-09-16 03:18:58 +02:00
Kristian S. Stangeland 62d9afd1d1 Readme spelling. 2012-09-16 02:43:31 +02:00
Kristian S. Stangeland 59a3ba82b8 Changed formating again. Added different levels. 2012-09-16 02:27:33 +02:00
Kristian S. Stangeland adb57e2bf3 Fixed readme formating somewhat. 2012-09-16 02:22:34 +02:00
Kristian S. Stangeland 798d734ba9 Adding readme. 2012-09-16 02:17:38 +02:00
Kristian S. Stangeland b299886b62 Fixed the old server listener. 2012-09-16 02:07:18 +02:00
Kristian S. Stangeland 913ff7eed1 Removed pointless imports. 2012-09-16 01:41:08 +02:00
Kristian S. Stangeland d04a666bc5 Readded a lighter version of network object injector.
This version only supports CraftBukkit 1.3.0 and later.
2012-09-15 23:07:14 +02:00
Kristian S. Stangeland bdfbfa47c8 The NetworkObjectInjector is too unstable, so we'll remove it. 2012-09-15 20:10:35 +02:00
Kristian S. Stangeland 57c720681a Made the object injector somewhat workable. 2012-09-15 20:09:34 +02:00
Kristian S. Stangeland 7e28aefb75 Moved the instance generator to a separate package.
In addition, added a new injection method. It's still not
finalized, and may get removed in the end.
2012-09-15 19:12:59 +02:00
Kristian S. Stangeland aa2dcefa0d Added two different player injection hooks, for compatibility with
TagAPI.
2012-09-15 13:47:14 +02:00
Kristian S. Stangeland 616213924b Refactored the SortedCopyOnWriteArray out into a separate package.
Made it extend the java.util.Collection interface.
2012-09-15 13:07:44 +02:00
Kristian S. Stangeland fffa3881dd We won't try to fix TagAPI. Instead, we'll warn the users. 2012-09-15 02:39:38 +02:00
Kristian S. Stangeland 85f7360109 Refactored the hacked array list out of the player injector. 2012-09-15 01:58:13 +02:00
Kristian S. Stangeland f2effab907 Improved documentation in a few places. 2012-09-15 01:04:08 +02:00
Kristian S. Stangeland 4505e31965 Prevented concurrency issues.
The SortedCopyOnWriteArray didn't wrap the iterator() in a
unmodifiable iterator. In addition, ConcurrentListenerMultimap
incorrectly used the iterator to remove objects from this Array. 

Added a "remove(T value)" method that is thread-safe.
2012-09-14 22:33:15 +02:00
Kristian S. Stangeland 88dcf0cb32 Improved API and performance.
API changes:
 * The PacketListener now uses a "ListeningWhitelist" class 
   to report which packet ids it wishes to listen in on for
   either the client or the server. This makes it possible to 
   use your plugin class as the listener more easily.
   
 * Added a priority system similar to Bukkit events.
   
Performance changes:
 * Create and maintain a separate list of listeners for each packet
   ID. This uses slightly more memory, but is far more efficient. 
   Especially in light of the priority system.

In addition, the listener lists are (hopefully) concurrent. They're
optimized for read-access, however. Adding or removing a listener
is a O(n) operation.
2012-09-14 19:12:08 +02:00
Kristian S. Stangeland bb8bec907c Added a priorty to every listener. Improved performance. 2012-09-14 12:41:57 +02:00
Kristian S. Stangeland 45169905fe Fix documentation. 2012-09-13 21:15:30 +02:00
Kristian S. Stangeland 783281ca2c Made "metrics" invisible. No point polluting the namespace. 2012-09-13 21:08:31 +02:00
Kristian S. Stangeland 0d35ab6038 Adding the license information. Using GPL2 as of now.
While libraries are sometimes licensed under LGPL, or even less
restrictive licenses, in this particular case it doesn't really
make sense to encourage direct copying. The problem is that 
competing libraries would conflict with each other. It's better
to feed improvements back into a single project.

Though, it's of course possible to fork this project. You can change
the internal classes - for instance, improving or changing the hook
points - without worriying about the public API.
2012-09-13 21:04:46 +02:00
Kristian S. Stangeland 8bb397b90e Adding the ItemDisguise example program. 2012-09-13 20:47:21 +02:00
Kristian S. Stangeland 0653dc8c15 Made the "getPlugin" method mandatory for all listeners.
It's pretty useful for the data collector, as well as the utility
method "removePacketListeners".
2012-09-13 17:29:37 +02:00
Kristian S. Stangeland ed5ce19795 Added a plugin users report to metrics. 2012-09-13 17:06:48 +02:00
Kristian S. Stangeland 83f313ce89 Switching to Metrics full. 2012-09-13 16:46:11 +02:00
Kristian S. Stangeland c02ffb569d Adding metrics (lite). 2012-09-13 16:08:55 +02:00
Kristian S. Stangeland 1710ef343b Syncronize before hacking into the packet list. In addition,
copy over any unprocessed packets into the new list.

When we're done, we'll also clean up properly by copying over 
any remaining packets.
2012-09-13 16:08:41 +02:00
Kristian S. Stangeland 5a6f3be219 Fix the server packet cancelling feature. 2012-09-13 14:56:14 +02:00
Kristian S. Stangeland 16d231f778 Fixed test case. 2012-09-13 14:43:15 +02:00
Kristian S. Stangeland 0d531ecc4b Added a custom exception. Simplified the event API. 2012-09-13 14:42:37 +02:00
Kristian S. Stangeland 353302fe5f Changed the API slightly. Added WorldType to PacketContainer. 2012-09-13 14:25:01 +02:00
Kristian S. Stangeland 035277bdeb Added support for at least Beta 1.8.
This involved a bunch of reflection magic, along with the removal
of every Apache Commons reference, in addition to every internal
Guava class.
2012-09-13 13:57:22 +02:00
Kristian S. Stangeland f372b247f9 Add support for 1.3.2 2012-09-13 11:34:47 +02:00
Kristian S. Stangeland 4307cb9104 Added the default instance class. 2012-09-13 11:01:32 +02:00
Kristian S. Stangeland da694ca5ed Added support for default values in packets. 2012-09-13 11:01:08 +02:00
48 changed files with 4710 additions and 309 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<classpathentry kind="lib" path="D:/Games/Minecraft/Server Mods/API/bukkit-1.3.1-R2.0.jar"/>
<classpathentry kind="lib" path="/ProtocolLib/bin/ProtocolLib.jar"/>
<classpathentry kind="output" path="class"/>
</classpath>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ItemDisguise</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,11 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.7
@@ -0,0 +1,119 @@
/*
* ItemDisguise - A simple Bukkit plugin that illustrates how to use ProtocolLib.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.itemdisguise;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.ConnectionSide;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldAccessException;
public class HideEnchantmentsListener {
private final Server server;
private final Logger logger;
public HideEnchantmentsListener(Server server, Logger logger) {
this.server = server;
this.logger = logger;
}
public void addListener(ProtocolManager protocolManager, JavaPlugin plugin) {
// Hide all enchantments
protocolManager.addPacketListener(new PacketAdapter(plugin, ConnectionSide.SERVER_SIDE, 0x67, 0x68, 0x3E) {
@Override
public void onPacketSending(PacketEvent event) {
PacketContainer packet = event.getPacket();
try {
// Item packets
switch (event.getPacketID()) {
case 0x67: // Set slot
removeEnchantments(packet.getItemModifier().read(0));
break;
case 0x68: // Set Window Items
ItemStack[] elements = packet.getItemArrayModifier().read(0);
for (int i = 0; i < elements.length; i++) {
if (elements[i] != null) {
removeEnchantments(elements[i]);
}
}
break;
case 0x3E: // Sound effect
event.setCancelled(true);
break;
}
} catch (FieldAccessException e) {
logger.log(Level.SEVERE, "Couldn't access field.", e);
}
}
});
// Censor
protocolManager.addPacketListener(new PacketAdapter(plugin, ConnectionSide.CLIENT_SIDE, 0x3) {
@Override
public void onPacketReceiving(PacketEvent event) {
if (event.getPacketID() == 0x3) {
try {
String message = event.getPacket().
getSpecificModifier(String.class).read(0);
if (message.contains("shit") || message.contains("fuck")) {
event.setCancelled(true);
event.getPlayer().sendMessage("Bad manners!");
}
} catch (FieldAccessException e) {
logger.log(Level.SEVERE, "Couldn't access field.", e);
}
}
}
});
}
public Server getServer() {
return server;
}
public void removeListener(ProtocolManager protocolManager, JavaPlugin plugin) {
// Just remove every adapter with this plugin
protocolManager.removePacketListeners(plugin);
}
private void removeEnchantments(ItemStack stack) {
Object[] copy = stack.getEnchantments().keySet().toArray();
for (Object enchantment : copy) {
stack.removeEnchantment((Enchantment) enchantment);
}
}
}
@@ -0,0 +1,99 @@
/*
* ItemDisguise - A simple Bukkit plugin that illustrates how to use ProtocolLib.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.itemdisguise;
import java.util.logging.Logger;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
public class ItemDisguiseMod extends JavaPlugin {
private ProtocolManager protocolManager;
private Logger logger;
private HideEnchantmentsListener enchantmentsListener;
@Override
public void onEnable() {
logger = getLoggerSafely();
protocolManager = ProtocolLibrary.getProtocolManager();
enchantmentsListener = new HideEnchantmentsListener(getServer(), logger);
enchantmentsListener.addListener(protocolManager, this);
}
@Override
public void onDisable() {
enchantmentsListener.removeListener(protocolManager, this);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player) {
Player player = (Player) sender;
if (label.equalsIgnoreCase("explosion")) {
PacketContainer fakeExplosion = protocolManager.createPacket(0x3C);
// Set the coordinates
try {
fakeExplosion.getSpecificModifier(double.class).
write(0, player.getLocation().getX()).
write(1, player.getLocation().getY()).
write(2, player.getLocation().getZ());
fakeExplosion.getSpecificModifier(float.class).
write(0, 3.0F);
protocolManager.sendServerPacket(player, fakeExplosion);
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
// Get the Bukkit logger first, before we try to create our own
private Logger getLoggerSafely() {
Logger log = null;
try {
log = getLogger();
} catch (Throwable e) {
// We'll handle it
}
if (log == null)
log = Logger.getLogger("Minecraft");
return log;
}
}
+14
View File
@@ -0,0 +1,14 @@
name: ItemDisguiseMod
version: 1.0.0
description: Change the appearance of inventory items.
author: Comphenix
website: http://www.comphenix.net/ItemDisguise
main: com.comphenix.itemdisguise.ItemDisguiseMod
depends: [ProtocolLib]
database: false
commands:
explosion:
description: Creates a fake explosion around the caller.
usage: /<command>
@@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol;
public class Application {
@@ -0,0 +1,198 @@
package com.comphenix.protocol;
import com.comphenix.protocol.reflect.IntEnum;
/**
* List of known packet IDs since 1.3.2.
*
* @author Kristian
*/
public final class Packets {
/**
* List of packets sent only by the server.
* @author Kristian
*/
public static final class Server extends IntEnum {
/**
* The singleton instance. Can also be retrieved from the parent class.
*/
private static Server INSTANCE = new Server();
public static final int KEEP_ALIVE = 0;
public static final int LOGIN = 1;
public static final int CHAT = 3;
public static final int UPDATE_TIME = 4;
public static final int ENTITY_EQUIPMENT = 5;
public static final int SPAWN_POSITION = 6;
public static final int UPDATE_HEALTH = 8;
public static final int RESPAWN = 9;
public static final int FLYING = 10;
public static final int PLAYER_POSITION = 11;
public static final int PLAYER_LOOK = 12;
public static final int PLAYER_LOOK_MOVE = 13;
public static final int ENTITY_LOCATION_ACTION = 17;
public static final int ARM_ANIMATION = 18;
public static final int NAMED_ENTITY_SPAWN = 20;
public static final int PICKUP_SPAWN = 21;
public static final int COLLECT = 22;
public static final int VEHICLE_SPAWN = 23;
public static final int MOB_SPAWN = 24;
public static final int ENTITY_PAINTING = 25;
public static final int ADD_EXP_ORB = 26;
public static final int ENTITY_VELOCITY = 28;
public static final int DESTROY_ENTITY = 29;
public static final int ENTITY = 30;
public static final int REL_ENTITY_MOVE = 31;
public static final int ENTITY_LOOK = 32;
public static final int REL_ENTITY_MOVE_LOOK = 33;
public static final int ENTITY_TELEPORT = 34;
public static final int ENTITY_HEAD_ROTATION = 35;
public static final int ENTITY_STATUS = 38;
public static final int ATTACH_ENTITY = 39;
public static final int ENTITY_METADATA = 40;
public static final int MOB_EFFECT = 41;
public static final int REMOVE_MOB_EFFECT = 42;
public static final int SET_EXPERIENCE = 43;
public static final int MAP_CHUNK = 51;
public static final int MULTI_BLOCK_CHANGE = 52;
public static final int BLOCK_CHANGE = 53;
public static final int PLAY_NOTE_BLOCK = 54;
public static final int BLOCK_BREAK_ANIMATION = 55;
public static final int MAP_CHUNK_BULK = 56;
public static final int EXPLOSION = 60;
public static final int WORLD_EVENT = 61;
public static final int NAMED_SOUND_EFFECT = 62;
public static final int BED = 70;
public static final int WEATHER = 71;
public static final int OPEN_WINDOW = 100;
public static final int CLOSE_WINDOW = 101;
public static final int SET_SLOT = 103;
public static final int WINDOW_ITEMS = 104;
public static final int CRAFT_PROGRESS_BAR = 105;
public static final int TRANSACTION = 106;
public static final int SET_CREATIVE_SLOT = 107;
public static final int UPDATE_SIGN = 130;
public static final int ITEM_DATA = 131;
public static final int TILE_ENTITY_DATA = 132;
public static final int STATISTIC = 200;
public static final int PLAYER_INFO = 201;
public static final int ABILITIES = 202;
public static final int TAB_COMPLETE = 203;
public static final int CUSTOM_PAYLOAD = 250;
public static final int KEY_RESPONSE = 252;
public static final int KEY_REQUEST = 253;
public static final int KICK_DISCONNECT = 255;
/**
* A registry that parses between names and packet IDs.
* @return The current server registry.
*/
public static Server getRegistry() {
return INSTANCE;
}
// We only allow a single instance of this class
private Server() {
super();
}
}
/**
* List of packets sent by the client.
* @author Kristian
*/
public static class Client extends IntEnum {
/**
* The singleton instance. Can also be retrieved from the parent class.
*/
private static Client INSTANCE = new Client();
public static final int KEEP_ALIVE = 0;
public static final int LOGIN = 1;
public static final int HANDSHAKE = 2;
public static final int CHAT = 3;
public static final int USE_ENTITY = 7;
public static final int RESPAWN = 9;
public static final int FLYING = 10;
public static final int PLAYER_POSITION = 11;
public static final int PLAYER_LOOK = 12;
public static final int PLAYER_LOOK_MOVE = 13;
public static final int BLOCK_DIG = 14;
public static final int PLACE = 15;
public static final int BLOCK_ITEM_SWITCH = 16;
public static final int ARM_ANIMATION = 18;
public static final int ENTITY_ACTION = 19;
public static final int CLOSE_WINDOW = 101;
public static final int WINDOW_CLICK = 102;
public static final int TRANSACTION = 106;
public static final int SET_CREATIVE_SLOT = 107;
public static final int BUTTON_CLICK = 108;
public static final int UPDATE_SIGN = 130;
public static final int ABILITIES = 202;
public static final int TAB_COMPLETE = 203;
public static final int LOCALE_AND_VIEW_DISTANCE = 204;
public static final int CLIENT_COMMAND = 205;
public static final int CUSTOM_PAYLOAD = 250;
public static final int KEY_RESPONSE = 252;
public static final int GET_INFO = 254;
public static final int KICK_DISCONNECT = 255;
/**
* A registry that parses between names and packet IDs.
* @return The current client registry.
*/
public static Client getRegistry() {
return INSTANCE;
}
// Like above
private Client() {
super();
}
}
/**
* A registry that parses between names and packet IDs.
* @return The current client registry.
*/
public static Server getServerRegistry() {
return Server.getRegistry();
}
/**
* A registry that parses between names and packet IDs.
* @return The current server registry.
*/
public static Client getClientRegistry() {
return Client.INSTANCE;
}
/**
* Find a packet by name. Must be capitalized and use underscores.
* @param name - name of packet to find.
* @return The packet ID found.
*/
public static int valueOf(String name) {
Integer serverAttempt = Server.INSTANCE.valueOf(name);
if (serverAttempt != null)
return serverAttempt;
else
return Client.INSTANCE.valueOf(name);
}
/**
* Retrieves the name of a packet.
* @param packetID - packet to retrieve name.
* @return The name, or NULL if unable to find such a packet.
*/
public static String getDeclaredName(int packetID) {
String serverAttempt = Server.INSTANCE.getDeclaredName(packetID);
if (serverAttempt != null)
return serverAttempt;
else
return Client.INSTANCE.getDeclaredName(packetID);
}
}
@@ -1,5 +1,24 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Server;
@@ -7,6 +26,7 @@ import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.injector.PacketFilterManager;
import com.comphenix.protocol.metrics.Statistics;
public class ProtocolLibrary extends JavaPlugin {
@@ -15,7 +35,10 @@ public class ProtocolLibrary extends JavaPlugin {
// Error logger
private Logger logger;
// Metrics and statistisc
private Statistics statistisc;
@Override
public void onLoad() {
logger = getLoggerSafely();
@@ -27,24 +50,62 @@ public class ProtocolLibrary extends JavaPlugin {
Server server = getServer();
PluginManager manager = server.getPluginManager();
// Notify server managers of incompatible plugins
checkForIncompatibility(manager);
// Player login and logout events
protocolManager.registerEvents(manager, this);
protocolManager.initializePlayers(server.getOnlinePlayers());
}
// Inject our hook into already existing players
protocolManager.initializePlayers(server.getOnlinePlayers());
// Try to enable statistics
try {
statistisc = new Statistics(this);
} catch (IOException e) {
logger.log(Level.SEVERE, "Unable to enable metrics.", e);
} catch (Throwable e) {
logger.log(Level.SEVERE, "Metrics cannot be enabled. Incompatible Bukkit version.", e);
}
}
private void checkForIncompatibility(PluginManager manager) {
// Plugin authors: Notify me to remove these
String[] incompatiblePlugins = { "TagAPI" };
for (String plugin : incompatiblePlugins) {
if (manager.getPlugin(plugin) != null) {
// Check for versions, ect.
logger.severe("Detected incompatible plugin: " + plugin);
}
}
}
@Override
public void onDisable() {
protocolManager.close();
protocolManager = null;
statistisc = null;
}
/**
* Retrieves the packet protocol manager.
* @return Packet protocol manager.
* @return Packet protocol manager, or NULL if it has been disabled.
*/
public static ProtocolManager getProtocolManager() {
return protocolManager;
}
/**
* Retrieve the metrics instance used to measure users of this library.
* <p>
* Note that this method may return NULL when the server is reloading or shutting down.
* @return Metrics instance container.
*/
public Statistics getStatistics() {
return statistisc;
}
// Get the Bukkit logger first, before we try to create our own
private Logger getLoggerSafely() {
@@ -1,34 +1,72 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Set;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.PacketConstructor;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.google.common.collect.ImmutableSet;
/**
* Represents an API for accessing the Minecraft protocol.
* @author Kristian
*/
public interface ProtocolManager {
/**
* Retrieves a list of every registered packet listener.
* @return Every registered packet listener.
*/
public abstract ImmutableSet<PacketListener> getPacketListeners();
public ImmutableSet<PacketListener> getPacketListeners();
/**
* Adds a packet listener.
* Adds a packet listener.
* <p>
* Adding an already registered listener has no effect. If you need to change the packets
* the current listener is observing, you must first remove the packet listener before you
* can register it again.
* @param listener - new packet listener.
*/
public abstract void addPacketListener(PacketListener listener);
public void addPacketListener(PacketListener listener);
/**
* Removes a given packet listener.
* Removes a given packet listener.
* <p>
* Attempting to remove a listener that doesn't exist has no effect.
* @param listener - the packet listener to remove.
*/
public abstract void removePacketListener(PacketListener listener);
public void removePacketListener(PacketListener listener);
/**
* Removes every listener associated with the given plugin.
* @param plugin - the plugin to unload.
*/
public void removePacketListeners(Plugin plugin);
/**
* Send a packet to the given player.
* @param reciever - the reciever.
@@ -75,15 +113,50 @@ public interface ProtocolManager {
* @return New encapsulated Minecraft packet.
*/
public PacketContainer createPacket(int id);
/**
* Retieves a set of every enabled packet.
* @return Every packet filter.
* Constructs a new encapsulated Minecraft packet with the given ID.
* <p>
* If set to true, the <i>forceDefaults</i> option will force the system to automatically
* give non-primitive fields in the packet sensible default values. For instance, certain
* packets - like Packet60Explosion - require a List or Set to be non-null. If the
* forceDefaults option is true, the List or Set will be automatically created.
*
* @param id - packet ID.
* @param forceDefaults - TRUE to use sensible defaults in most fields, FALSE otherwise.
* @return New encapsulated Minecraft packet.
*/
public Set<Integer> getPacketFilters();
public PacketContainer createPacket(int id, boolean forceDefaults);
/**
* Determines whether or not is protocol mananger has been disabled.
* Construct a packet using the special builtin Minecraft constructors.
* @param id - the packet ID.
* @param arguments - arguments that will be passed to the constructor.
* @return The packet constructor.
*/
public PacketConstructor createPacketConstructor(int id, Object... arguments);
/**
* Completely refresh all clients about an entity.
* @param entity - entity to refresh.
* @param observers - the clients to update.
*/
public void updateEntity(Entity entity, List<Player> observers) throws FieldAccessException;
/**
* Retrieves a immutable set containing the ID of the sent server packets that will be observed by listeners.
* @return Every filtered server packet.
*/
public Set<Integer> getSendingFilters();
/**
* Retrieves a immutable set containing the ID of the recieved client packets that will be observed by listeners.
* @return Every filtered client packet.
*/
public Set<Integer> getReceivingFilters();
/**
* Determines whether or not this protocol mananger has been disabled.
* @return TRUE if it has, FALSE otherwise.
*/
public boolean isClosed();
@@ -0,0 +1,223 @@
package com.comphenix.protocol.concurrency;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import com.google.common.base.Objects;
import com.google.common.collect.Iterables;
/**
* An implicitly sorted array list that preserves insertion order and maintains duplicates.
* @param <T> - type of the elements in the list.
*/
public class SortedCopyOnWriteArray<T extends Comparable<T>> implements Iterable<T>, Collection<T> {
// Prevent reordering
private volatile List<T> list;
/**
* Construct an empty sorted array.
*/
public SortedCopyOnWriteArray() {
list = new ArrayList<T>();
}
/**
* Create a sorted array from the given list. The elements will be automatically sorted.
* @param wrapped - the collection whose elements are to be placed into the list.
*/
public SortedCopyOnWriteArray(Collection<T> wrapped) {
this.list = new ArrayList<T>(wrapped);
}
/**
* Create a sorted array from the given list.
* @param wrapped - the collection whose elements are to be placed into the list.
* @param sort - TRUE to automatically sort the collection, FALSE if it is already sorted.
*/
public SortedCopyOnWriteArray(Collection<T> wrapped, boolean sort) {
this.list = new ArrayList<T>(wrapped);
if (sort) {
Collections.sort(list);
}
}
/**
* Inserts the given element in the proper location.
* @param value - element to insert.
*/
@Override
public synchronized boolean add(T value) {
// We use NULL as a special marker, so we don't allow it
if (value == null)
throw new IllegalArgumentException("value cannot be NULL");
List<T> copy = new ArrayList<T>();
for (T element : list) {
// If the value is now greater than the current element, it should be placed right before it
if (value != null && value.compareTo(element) < 0) {
copy.add(value);
value = null;
}
copy.add(element);
}
// Don't forget to add it
if (value != null)
copy.add(value);
list = copy;
return true;
}
@Override
public synchronized boolean addAll(Collection<? extends T> values) {
if (values == null)
throw new IllegalArgumentException("values cannot be NULL");
if (values.size() == 0)
return false;
List<T> copy = new ArrayList<T>();
// Insert the new content and sort it
copy.addAll(list);
copy.addAll(values);
Collections.sort(copy);
list = copy;
return true;
}
/**
* Removes from the list by making a new list with every element except the one given.
* <p>
* Objects will be compared using the given objects equals() method.
* @param value - value to remove.
*/
@Override
public synchronized boolean remove(Object value) {
List<T> copy = new ArrayList<T>();
// Note that there's not much to be gained from using BinarySearch, as we
// have to copy (and thus read) the entire list regardless.
// Copy every element except the one given to us.
for (T element : list) {
if (value != null && !Objects.equal(value, element)) {
copy.add(element);
value = null;
}
}
list = copy;
return value == null;
}
@Override
public boolean removeAll(Collection<?> values) {
// Special cases
if (values == null)
throw new IllegalArgumentException("values cannot be NULL");
if (values.size() == 0)
return false;
List<T> copy = new ArrayList<T>();
copy.addAll(list);
copy.removeAll(values);
list = copy;
return true;
}
@Override
public boolean retainAll(Collection<?> values) {
// Special cases
if (values == null)
throw new IllegalArgumentException("values cannot be NULL");
if (values.size() == 0)
return false;
List<T> copy = new ArrayList<T>();
copy.addAll(list);
copy.removeAll(values);
list = copy;
return true;
}
/**
* Removes from the list by making a copy of every element except the one with the given index.
* @param index - index of the element to remove.
*/
public synchronized void remove(int index) {
List<T> copy = new ArrayList<T>(list);
copy.remove(index);
list = copy;
}
/**
* Retrieves an element by index.
* @param index - index of element to retrieve.
* @return The element at the given location.
*/
public T get(int index) {
return list.get(index);
}
/**
* Retrieve the size of the list.
* @return Size of the list.
*/
public int size() {
return list.size();
}
/**
* Retrieves an iterator over the elements in the given list.
* Warning: No not attempt to remove elements using the iterator.
*/
public Iterator<T> iterator() {
return Iterables.unmodifiableIterable(list).iterator();
}
@Override
public void clear() {
list = new ArrayList<T>();
}
@Override
public boolean contains(Object value) {
return list.contains(value);
}
@Override
public boolean containsAll(Collection<?> values) {
return list.containsAll(values);
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@SuppressWarnings("hiding")
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
}
@@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
/**
@@ -0,0 +1,68 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
/**
* Represents a packet event priority, similar to the Bukkit EventPriority.
*
* @author Kristian
*/
public enum ListenerPriority {
/**
* Event call is of very low importance and should be ran first, to allow
* other plugins to further customise the outcome.
*/
LOWEST(0),
/**
* Event call is of low importance.
*/
LOW(1),
/**
* Event call is neither important or unimportant, and may be ran normally.
*/
NORMAL(2),
/**
* Event call is of high importance.
*/
HIGH(3),
/**
* Event call is critical and must have the final say in what happens to the
* event.
*/
HIGHEST(4),
/**
* Event is listened to purely for monitoring the outcome of an event.
* <p/>
* No modifications to the event should be made under this priority.
*/
MONITOR(5);
private final int slot;
private ListenerPriority(int slot) {
this.slot = slot;
}
/**
* A low slot represents a low priority.
* @return Integer representation of this priorty.
*/
public int getSlot() {
return slot;
}
}
@@ -0,0 +1,110 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import java.util.Set;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
/**
* Determines which packets will be observed by a listener, and with what priority.
* @author Kristian
*/
public class ListeningWhitelist {
/**
* A whitelist with no packets - indicates that the listener shouldn't observe any packets.
*/
public static ListeningWhitelist EMPTY_WHITELIST = new ListeningWhitelist(ListenerPriority.LOW);
private ListenerPriority priority;
private Set<Integer> whitelist;
/**
* Creates a packet whitelist for a given priority with a set of packet IDs.
* @param priority - the listener priority.
* @param whitelist - set of IDs to observe/enable.
*/
public ListeningWhitelist(ListenerPriority priority, Set<Integer> whitelist) {
this.priority = priority;
this.whitelist = whitelist;
}
/**
* Creates a packet whitelist of a given priority for a list of packets.
* @param priority - the listener priority.
* @param whitelist - list of packet IDs to observe/enable.
*/
public ListeningWhitelist(ListenerPriority priority, Integer... whitelist) {
this.priority = priority;
this.whitelist = Sets.newHashSet(whitelist);
}
/**
* Whether or not this whitelist has any enabled packets.
* @return TRUE if there are any packets, FALSE otherwise.
*/
public boolean isEnabled() {
return whitelist != null || whitelist.size() > 0;
}
/**
* Retrieve the priority in the execution order of the packet listener. Highest priority will be executed last.
* @return Execution order in terms of priority.
*/
public ListenerPriority getPriority() {
return priority;
}
/**
* Retrieves the list of packets that will be observed by the listeners.
* @return Packet whitelist.
*/
public Set<Integer> getWhitelist() {
return whitelist;
}
@Override
public int hashCode(){
return Objects.hashCode(priority, whitelist);
}
@Override
public boolean equals(final Object obj){
if(obj instanceof ListeningWhitelist){
final ListeningWhitelist other = (ListeningWhitelist) obj;
return Objects.equal(priority, other.priority)
&& Objects.equal(whitelist, other.whitelist);
} else{
return false;
}
}
@Override
public String toString() {
if (this == EMPTY_WHITELIST)
return "EMPTY_WHITELIST";
else
return Objects.toStringHelper(this)
.add("priority", priority)
.add("packets", whitelist).toString();
}
}
@@ -1,11 +1,23 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.collect.Sets;
import org.bukkit.plugin.Plugin;
/**
* Represents a packet listener with useful constructors.
@@ -14,20 +26,46 @@ import com.google.common.collect.Sets;
*/
public abstract class PacketAdapter implements PacketListener {
protected JavaPlugin plugin;
protected Set<Integer> packetsID;
protected Plugin plugin;
protected ConnectionSide connectionSide;
protected ListeningWhitelist receivingWhitelist = ListeningWhitelist.EMPTY_WHITELIST;
protected ListeningWhitelist sendingWhitelist = ListeningWhitelist.EMPTY_WHITELIST;
/**
* Initialize a packet listener.
* Initialize a packet listener with default priority.
* @param plugin - the plugin that spawned this listener.
* @param connectionSide - the packet type the listener is looking for.
* @param packets - the packet IDs the listener is looking for.
*/
public PacketAdapter(JavaPlugin plugin, ConnectionSide connectionSide, Integer... packets) {
public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, Integer... packets) {
this(plugin, connectionSide, ListenerPriority.NORMAL, packets);
}
/**
* Initialize a packet listener for a single connection side.
* @param plugin - the plugin that spawned this listener.
* @param connectionSide - the packet type the listener is looking for.
* @param listenerPriority - the event priority.
* @param packets - the packet IDs the listener is looking for.
*/
public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerPriority listenerPriority, Integer... packets) {
if (plugin == null)
throw new IllegalArgumentException("plugin cannot be null");
if (connectionSide == null)
throw new IllegalArgumentException("connectionSide cannot be null");
if (listenerPriority == null)
throw new IllegalArgumentException("listenerPriority cannot be null");
if (packets == null)
throw new IllegalArgumentException("packets cannot be null");
// Add whitelists
if (connectionSide.isForServer())
sendingWhitelist = new ListeningWhitelist(listenerPriority, packets);
if (connectionSide.isForClient())
receivingWhitelist = new ListeningWhitelist(listenerPriority, packets);
this.plugin = plugin;
this.connectionSide = connectionSide;
this.packetsID = Sets.newHashSet(packets);
}
@Override
@@ -41,20 +79,46 @@ public abstract class PacketAdapter implements PacketListener {
}
@Override
public ConnectionSide getConnectionSide() {
return connectionSide;
public ListeningWhitelist getReceivingWhitelist() {
return receivingWhitelist;
}
@Override
public Set<Integer> getPacketsID() {
return packetsID;
public ListeningWhitelist getSendingWhitelist() {
return sendingWhitelist;
}
@Override
public String toString() {
public Plugin getPlugin() {
return plugin;
}
/**
* Retrieves the name of the plugin that has been associated with the listener.
* @return Name of the associated plugin.
*/
public static String getPluginName(PacketListener listener) {
Plugin plugin = listener.getPlugin();
// Try to get the plugin name
try {
if (plugin == null)
return "UNKNOWN";
else
return plugin.getName();
} catch (NoSuchMethodError e) {
return plugin.toString();
}
}
@Override
public String toString() {
// This is used by the error reporter
return String.format("PacketAdapter[plugin=%s, side=%s, packets=%s]",
plugin.getName(), getConnectionSide().name(),
StringUtils.join(packetsID, ", "));
return String.format("PacketAdapter[plugin=%s, sending=%s, receiving=%s]",
getPluginName(this),
sendingWhitelist,
receivingWhitelist);
}
}
@@ -1,11 +1,36 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import org.apache.commons.lang.NullArgumentException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import com.comphenix.protocol.injector.StructureCache;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
import net.minecraft.server.Packet;
@@ -22,6 +47,20 @@ public class PacketContainer {
// Current structure modifier
protected StructureModifier<Object> structureModifier;
// Check whether or not certain classes exists
private static boolean hasWorldType = false;
// The getEntity method
private static Method getEntity;
static {
try {
Class.forName("net.minecraft.server.WorldType");
hasWorldType = true;
} catch (ClassNotFoundException e) {
}
}
/**
* Creates a packet container for a new packet.
@@ -48,7 +87,7 @@ public class PacketContainer {
*/
public PacketContainer(int id, Packet handle, StructureModifier<Object> structure) {
if (handle == null)
throw new NullArgumentException("handle");
throw new IllegalArgumentException("handle cannot be null.");
this.id = id;
this.handle = handle;
@@ -71,10 +110,22 @@ public class PacketContainer {
return structureModifier;
}
public <T> StructureModifier<T> getPrimitiveModifier(Class<T> primitiveType) {
/**
* Retrieves a read/write structure for every field with the given type.
* @param primitiveType - the type to find.
* @return A modifier for this specific type.
*/
public <T> StructureModifier<T> getSpecificModifier(Class<T> primitiveType) {
return structureModifier.withType(primitiveType);
}
/**
* Retrieves a read/write structure for ItemStack.
* <p>
* This modifier will automatically marshall between the Bukkit ItemStack and the
* internal Minecraft ItemStack.
* @return A modifier for ItemStack fields.
*/
public StructureModifier<ItemStack> getItemModifier() {
// Convert from and to the Bukkit wrapper
return structureModifier.<ItemStack>withType(net.minecraft.server.ItemStack.class, new EquivalentConverter<ItemStack>() {
@@ -86,9 +137,21 @@ public class PacketContainer {
public ItemStack getSpecific(Object generic) {
return new CraftItemStack((net.minecraft.server.ItemStack) generic);
}
@Override
public Class<ItemStack> getSpecificType() {
return ItemStack.class;
}
});
}
/**
* Retrieves a read/write structure for arrays of ItemStacks.
* <p>
* This modifier will automatically marshall between the Bukkit ItemStack and the
* internal Minecraft ItemStack.
* @return A modifier for ItemStack array fields.
*/
public StructureModifier<ItemStack[]> getItemArrayModifier() {
// Convert to and from the Bukkit wrapper
return structureModifier.<ItemStack[]>withType(net.minecraft.server.ItemStack[].class, new EquivalentConverter<ItemStack[]>() {
@@ -113,6 +176,111 @@ public class PacketContainer {
}
return result;
}
@Override
public Class<ItemStack[]> getSpecificType() {
return ItemStack[].class;
}
});
}
/**
* Retrieves a read/write structure for the world type enum.
* <p>
* This modifier will automatically marshall between the Bukkit world type and the
* internal Minecraft world type.
* @return A modifier for world type fields.
*/
public StructureModifier<WorldType> getWorldTypeModifier() {
if (!hasWorldType) {
// We couldn't find the Minecraft equivalent
return structureModifier.withType(null);
}
// Convert to and from the Bukkit wrapper
return structureModifier.<WorldType>withType(net.minecraft.server.WorldType.class, new EquivalentConverter<WorldType>() {
@Override
public Object getGeneric(WorldType specific) {
return net.minecraft.server.WorldType.getType(specific.getName());
}
@Override
public WorldType getSpecific(Object generic) {
net.minecraft.server.WorldType type = (net.minecraft.server.WorldType) generic;
return WorldType.getByName(type.name());
}
@Override
public Class<WorldType> getSpecificType() {
return WorldType.class;
}
});
}
/**
* Retrieves a read/write structure for entity objects.
* <p>
* Note that entities are transmitted by integer ID, and the type may not be enough
* to distinguish between entities and other values. Thus, this structure modifier
* MAY return null or invalid entities for certain fields. Using the correct index
* is essential.
*
* @return A modifier entity types.
*/
public StructureModifier<Entity> getEntityModifier(World world) {
final Object worldServer = ((CraftWorld) world).getHandle();
final Class<?> nmsEntityClass = net.minecraft.server.Entity.class;
final World worldCopy = world;
if (getEntity == null)
getEntity = FuzzyReflection.fromObject(worldServer).getMethodByParameters(
"getEntity", nmsEntityClass, new Class[] { int.class });
// Convert to and from the Bukkit wrapper
return structureModifier.<Entity>withType(int.class, new EquivalentConverter<Entity>() {
@Override
public Object getGeneric(Entity specific) {
// Simple enough
return specific.getEntityId();
}
@Override
public Entity getSpecific(Object generic) {
try {
net.minecraft.server.Entity nmsEntity = (net.minecraft.server.Entity)
getEntity.invoke(worldServer, generic);
Integer id = (Integer) generic;
// Attempt to get the Bukkit entity
if (nmsEntity != null) {
return nmsEntity.getBukkitEntity();
} else {
// Maybe it's a player that's just logged in? Try a search
for (Player player : worldCopy.getPlayers()) {
if (player.getEntityId() == id) {
return player;
}
}
System.out.println("Entity doesn't exist.");
return null;
}
} catch (IllegalArgumentException e) {
throw new RuntimeException("Incorrect arguments detected.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot read field due to a security limitation.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error occured in Minecraft method.", e.getCause());
}
}
@Override
public Class<Entity> getSpecificType() {
return Entity.class;
}
});
}
@@ -1,18 +1,36 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import java.util.EventObject;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
public class PacketEvent extends EventObject {
public class PacketEvent extends EventObject implements Cancellable {
/**
* Automatically generated by Eclipse.
*/
private static final long serialVersionUID = -5360289379097430620L;
private PacketContainer packet;
private Player sender;
private Player reciever;
private Player player;
private boolean serverPacket;
private boolean cancel;
/**
@@ -23,11 +41,11 @@ public class PacketEvent extends EventObject {
super(source);
}
private PacketEvent(Object source, PacketContainer packet, Player sender, Player reciever) {
private PacketEvent(Object source, PacketContainer packet, Player player, boolean serverPacket) {
super(source);
this.packet = packet;
this.sender = sender;
this.reciever = reciever;
this.player = player;
this.serverPacket = serverPacket;
}
/**
@@ -38,7 +56,7 @@ public class PacketEvent extends EventObject {
* @return The event.
*/
public static PacketEvent fromClient(Object source, PacketContainer packet, Player client) {
return new PacketEvent(source, packet, client, null);
return new PacketEvent(source, packet, client, false);
}
/**
@@ -49,7 +67,7 @@ public class PacketEvent extends EventObject {
* @return The event.
*/
public static PacketEvent fromServer(Object source, PacketContainer packet, Player recipient) {
return new PacketEvent(source, packet, null, recipient);
return new PacketEvent(source, packet, recipient, true);
}
/**
@@ -93,27 +111,18 @@ public class PacketEvent extends EventObject {
}
/**
* Retrieves the player that has sent the packet.
* @return The sender, or NULL if the server is sending the packet.
* Retrieves the player that has sent the packet or is recieving it.
* @return The player associated with this event.
*/
public Player getSender() {
return sender;
public Player getPlayer() {
return player;
}
/**
* Retrieves the player that will recieve the packet.
* @return The reciever, or NULL if the server is recieving the packet.
*/
public Player getReciever() {
return reciever;
}
/**
* Whether or not this packet was created by the server.
* @return TRUE if the packet was created by the server, FALSE if it was created by a client.
*/
public boolean isServerPacket() {
return getReciever() != null;
return serverPacket;
}
}
@@ -1,8 +1,30 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import java.util.Set;
import org.bukkit.plugin.Plugin;
/**
* Represents a listener that recieves notifications when packets are sent or recieved.
* <p>
* Use {@link PacketAdapter} for a simple wrapper around this interface.
* @author Kristian
*/
public interface PacketListener {
/**
@@ -21,14 +43,20 @@ public interface PacketListener {
public void onPacketReceiving(PacketEvent event);
/**
* Retrieve whether or not we're listening for client or server packets.
* @return The type of packets we expect.
* Retrieve which packets sent by the server this listener will observe.
* @return List of server packets to observe, along with the priority.
*/
public ConnectionSide getConnectionSide();
public ListeningWhitelist getSendingWhitelist();
/**
* Set of packet ids we expect to recieve.
* @return Packets IDs.
* Retrieve which packets sent by the client this listener will observe.
* @return List of server packets to observe, along with the priority.
*/
public Set<Integer> getPacketsID();
public ListeningWhitelist getReceivingWhitelist();
/**
* Retrieve the plugin that created list packet listener.
* @return The plugin, or NULL if not available.
*/
public Plugin getPlugin();
}
@@ -0,0 +1,194 @@
package com.comphenix.protocol.injector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.comphenix.protocol.concurrency.SortedCopyOnWriteArray;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
import com.google.common.base.Objects;
import com.google.common.primitives.Ints;
/**
* A thread-safe implementation of a listener multimap.
*
* @author Kristian
*/
public class ConcurrentListenerMultimap {
// The core of our map
protected ConcurrentMap<Integer, SortedCopyOnWriteArray<PrioritizedListener>> listeners =
new ConcurrentHashMap<Integer, SortedCopyOnWriteArray<PrioritizedListener>>();
/**
* Adds a listener to its requested list of packet recievers.
* @param listener - listener with a list of packets to recieve notifcations for.
* @param whitelist - the packet whitelist to use.
*/
public void addListener(PacketListener listener, ListeningWhitelist whitelist) {
PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority());
for (Integer packetID : whitelist.getWhitelist()) {
addListener(packetID, prioritized);
}
}
// Add the listener to a specific packet notifcation list
private void addListener(Integer packetID, PrioritizedListener listener) {
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(packetID);
// We don't want to create this for every lookup
if (list == null) {
// It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order,
// which is a essential feature for our purposes.
final SortedCopyOnWriteArray<PrioritizedListener> value = new SortedCopyOnWriteArray<PrioritizedListener>();
list = listeners.putIfAbsent(packetID, value);
// We may end up creating multiple multisets, but we'll agree
// on the one to use.
if (list == null) {
list = value;
}
}
// Thread safe
list.add(listener);
}
/**
* Removes the given listener from the packet event list.
* @param listener - listener to remove.
* @param whitelist - the packet whitelist that was used.
* @return Every packet ID that was removed due to no listeners.
*/
public List<Integer> removeListener(PacketListener listener, ListeningWhitelist whitelist) {
List<Integer> removedPackets = new ArrayList<Integer>();
// Again, not terribly efficient. But adding or removing listeners should be a rare event.
for (Integer packetID : whitelist.getWhitelist()) {
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(packetID);
// Remove any listeners
if (list != null) {
// Don't remove from newly created lists
if (list.size() > 0) {
// Remove this listener. Note that priority is generally ignored.
list.remove(new PrioritizedListener(listener, whitelist.getPriority()));
if (list.size() == 0) {
listeners.remove(packetID);
removedPackets.add(packetID);
}
}
}
// Move on to the next
}
return removedPackets;
}
/**
* Invokes the given packet event for every registered listener.
* @param logger - the logger that will be used to inform about listener exceptions.
* @param event - the packet event to invoke.
*/
public void invokePacketRecieving(Logger logger, PacketEvent event) {
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(event.getPacketID());
if (list == null)
return;
// We have to be careful. Cannot modify the underlying list when sending notifications.
synchronized (list) {
for (PrioritizedListener element : list) {
try {
element.getListener().onPacketReceiving(event);
} catch (Throwable e) {
// Minecraft doesn't want your Exception.
logger.log(Level.SEVERE,
"Exception occured in onPacketReceiving() for " +
PacketAdapter.getPluginName(element.getListener()), e);
}
}
}
}
/**
* Invokes the given packet event for every registered listener.
* @param logger - the logger that will be used to inform about listener exceptions.
* @param event - the packet event to invoke.
*/
public void invokePacketSending(Logger logger, PacketEvent event) {
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(event.getPacketID());
if (list == null)
return;
synchronized (list) {
for (PrioritizedListener element : list) {
try {
element.getListener().onPacketSending(event);
} catch (Throwable e) {
// Minecraft doesn't want your Exception.
logger.log(Level.SEVERE,
"Exception occured in onPacketReceiving() for " +
PacketAdapter.getPluginName(element.getListener()), e);
}
}
}
}
/**
* A listener with an associated priority.
*/
private class PrioritizedListener implements Comparable<PrioritizedListener> {
private PacketListener listener;
private ListenerPriority priority;
public PrioritizedListener(PacketListener listener, ListenerPriority priority) {
this.listener = listener;
this.priority = priority;
}
@Override
public int compareTo(PrioritizedListener other) {
// This ensures that lower priority listeners are executed first
return Ints.compare(this.getPriority().getSlot(),
other.getPriority().getSlot());
}
// Note that this equals() method is NOT consistent with compareTo().
// But, it's a private class so who cares.
@Override
public boolean equals(Object obj) {
// We only care about the listener - priority itself should not make a difference
if(obj instanceof PrioritizedListener){
final PrioritizedListener other = (PrioritizedListener) obj;
return Objects.equal(listener, other.listener);
} else {
return false;
}
}
public PacketListener getListener() {
return listener;
}
public ListenerPriority getPriority() {
return priority;
}
}
}
@@ -0,0 +1,159 @@
package com.comphenix.protocol.injector;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import com.comphenix.protocol.injector.PacketConstructor.BukkitUnwrapper;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.collect.Lists;
/**
* Used to perform certain operations on entities.
*
* @author Kristian
*/
class EntityUtilities {
private static Field entityTrackerField;
private static Field trackedEntitiesField;
private static Field trackedPlayersField;
private static Method hashGetMethod;
private static Method scanPlayersMethod;
public static void updateEntity(Entity entity, List<Player> observers) throws FieldAccessException {
World world = entity.getWorld();
Object worldServer = ((CraftWorld) world).getHandle();
// We have to rely on the class naming here.
if (entityTrackerField == null)
entityTrackerField = FuzzyReflection.fromObject(worldServer).getFieldByType(".*Tracker");
// Get the tracker
Object tracker = null;
try {
tracker = FieldUtils.readField(entityTrackerField, worldServer, false);
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot access 'tracker' field due to security limitations.", e);
}
if (trackedEntitiesField == null) {
@SuppressWarnings("rawtypes")
Set<Class> ignoredTypes = new HashSet<Class>();
// Well, this is more difficult. But we're looking for a Minecraft object that is not
// created by the constructor(s).
for (Constructor<?> constructor : tracker.getClass().getConstructors()) {
for (Class<?> type : constructor.getParameterTypes()) {
ignoredTypes.add(type);
}
}
// The Minecraft field that's NOT filled in by the constructor
trackedEntitiesField = FuzzyReflection.fromObject(tracker, true).
getFieldByType(FuzzyReflection.MINECRAFT_OBJECT, ignoredTypes);
}
// Read the entity hashmap
Object trackedEntities = null;
try {
trackedEntities = FieldUtils.readField(trackedEntitiesField, tracker, true);
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot access 'trackedEntities' field due to security limitations.", e);
}
// Getting the "get" method is pretty hard, but first - try to just get it by name
if (hashGetMethod == null) {
Class<?> type = trackedEntities.getClass();
try {
hashGetMethod = type.getMethod("get", int.class);
} catch (NoSuchMethodException e) {
Class<?>[] params = { int.class };
// Then it's probably the lowest named method that takes an int-parameter
for (Method method : type.getMethods()) {
if (Arrays.equals(params, method.getParameterTypes())) {
if (hashGetMethod == null ||
method.getName().compareTo(hashGetMethod.getName()) < 0) {
hashGetMethod = method;
}
}
}
}
}
try {
//EntityTrackerEntry trackEntity = (EntityTrackerEntry) tracker.trackedEntities.get(entity.getEntityId());
Object trackerEntry = hashGetMethod.invoke(trackedEntities, entity.getEntityId());
if (trackedPlayersField == null) {
// This one is fairly easy
trackedPlayersField = FuzzyReflection.fromObject(trackerEntry).getFieldByType("java\\.util\\..*");
}
// Phew, finally there.
Collection<?> trackedPlayers = (Collection<?>) FieldUtils.readField(trackedPlayersField, trackerEntry, false);
List<Object> nmsPlayers = unwrapBukkit(observers);
// trackEntity.trackedPlayers.clear();
trackedPlayers.removeAll(nmsPlayers);
// We have to rely on a NAME once again. Damn it.
if (scanPlayersMethod == null) {
scanPlayersMethod = trackerEntry.getClass().getMethod("scanPlayers", List.class);
}
//trackEntity.scanPlayers(server.players);
scanPlayersMethod.invoke(trackerEntry, nmsPlayers);
} catch (IllegalArgumentException e) {
throw e;
} catch (IllegalAccessException e) {
throw new FieldAccessException("Security limitation prevents access to 'get' method in IntHashMap", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Exception occurred in Minecraft.", e);
} catch (SecurityException e) {
throw new FieldAccessException("Security limitation prevents access to 'scanPlayers' method in trackerEntry.", e);
} catch (NoSuchMethodException e) {
throw new FieldAccessException("Canot find 'scanPlayers' method. Is ProtocolLib up to date?", e);
}
}
private static List<Object> unwrapBukkit(List<Player> players) {
List<Object> output = Lists.newArrayList();
BukkitUnwrapper unwrapper = new BukkitUnwrapper();
// Get the NMS equivalent
for (Player player : players) {
Object result = unwrapper.unwrapItem(player);
if (result != null)
output.add(result);
else
throw new IllegalArgumentException("Cannot unwrap item " + player);
}
return output;
}
}
@@ -0,0 +1,115 @@
package com.comphenix.protocol.injector;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Set;
import net.minecraft.server.Packet;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import com.comphenix.protocol.injector.NetworkFieldInjector.FakePacket;
/**
* The array list that notifies when packets are sent by the server.
*
* @author Kristian
*/
class InjectedArrayList extends ArrayList<Packet> {
/**
* Silly Eclipse.
*/
private static final long serialVersionUID = -1173865905404280990L;
private PlayerInjector injector;
private Set<Packet> ignoredPackets;
private ClassLoader classLoader;
public InjectedArrayList(ClassLoader classLoader, PlayerInjector injector, Set<Packet> ignoredPackets) {
this.classLoader = classLoader;
this.injector = injector;
this.ignoredPackets = ignoredPackets;
}
@Override
public boolean add(Packet packet) {
Packet result = null;
// Check for fake packets and ignored packets
if (packet instanceof FakePacket) {
return true;
} else if (ignoredPackets.contains(packet)) {
ignoredPackets.remove(packet);
} else {
result = injector.handlePacketRecieved(packet);
}
// A NULL packet indicate cancelling
try {
if (result != null) {
super.add(result);
} else {
// We'll use the FakePacket marker instead of preventing the filters
injector.sendServerPacket(createNegativePacket(packet), true);
}
// Collection.add contract
return true;
} catch (InvocationTargetException e) {
throw new RuntimeException("Reverting cancelled packet failed.", e.getTargetException());
}
}
/**
* Used by a hack that reverses the effect of a cancelled packet. Returns a packet
* whereby every int method's return value is inverted (a => -a).
*
* @param source - packet to invert.
* @return The inverted packet.
*/
Packet createNegativePacket(Packet source) {
Enhancer ex = new Enhancer();
Class<?> type = source.getClass();
// We want to subtract the byte amount that were added to the running
// total of outstanding packets. Otherwise, cancelling too many packets
// might cause a "disconnect.overflow" error.
//
// We do that by constructing a special packet of the same type that returns
// a negative integer for all zero-parameter integer methods. This includes the
// size() method, which is used by the queue method to count the number of
// bytes to add.
//
// Essentially, we have:
//
// public class NegativePacket extends [a packet] {
// @Override
// public int size() {
// return -super.size();
// }
// ect.
// }
ex.setInterfaces(new Class[] { FakePacket.class } );
ex.setUseCache(true);
ex.setClassLoader(classLoader);
ex.setSuperclass(type);
ex.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (method.getReturnType().equals(int.class) && args.length == 0) {
Integer result = (Integer) proxy.invokeSuper(obj, args);
return -result;
} else {
return proxy.invokeSuper(obj, args);
}
}
});
return (Packet) ex.create();
}
}
@@ -1,15 +1,31 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.ObjectUtils;
import net.minecraft.server.Packet;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.base.Objects;
/**
* Static registries in Minecraft.
@@ -78,7 +94,7 @@ class MinecraftRegistry {
// Will most likely not be used
for (Map.Entry<Class, Integer> entry : getPacketToID().entrySet()) {
if (ObjectUtils.equals(entry.getValue(), packetID)) {
if (Objects.equal(entry.getValue(), packetID)) {
return entry.getKey();
}
}
@@ -0,0 +1,145 @@
package com.comphenix.protocol.injector;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.entity.Player;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.VolatileField;
import com.google.common.collect.Sets;
import net.minecraft.server.Packet;
/**
* Injection hook that overrides the packet queue lists in NetworkHandler.
*
* @author Kristian
*/
class NetworkFieldInjector extends PlayerInjector {
/**
* Marker interface that indicates a packet is fake and should not be processed.
* @author Kristian
*/
public interface FakePacket {
// Nothing
}
// Packets to ignore
private Set<Packet> ignoredPackets = Sets.newSetFromMap(new ConcurrentHashMap<Packet, Boolean>());
// Overridden fields
private List<VolatileField> overridenLists = new ArrayList<VolatileField>();
// Sync field
private static Field syncField;
private Object syncObject;
public NetworkFieldInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
super(player, manager, sendingFilters);
}
@Override
protected void initialize() throws IllegalAccessException {
super.initialize();
// Get the sync field as well
if (hasInitialized) {
if (syncField == null)
syncField = FuzzyReflection.fromObject(networkManager, true).getFieldByType("java\\.lang\\.Object");
syncObject = FieldUtils.readField(syncField, networkManager, true);
}
}
@Override
public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException {
if (networkManager != null) {
try {
if (!filtered) {
ignoredPackets.add(packet);
}
// Note that invocation target exception is a wrapper for a checked exception
queueMethod.invoke(networkManager, packet);
} catch (IllegalArgumentException e) {
throw e;
} catch (InvocationTargetException e) {
throw e;
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unable to access queue method.", e);
}
} else {
throw new IllegalStateException("Unable to load network mananager. Cannot send packet.");
}
}
@Override
public void injectManager() {
if (networkManager != null) {
@SuppressWarnings("rawtypes")
StructureModifier<List> list = networkModifier.withType(List.class);
// Subclass both send queues
for (Field field : list.getFields()) {
VolatileField overwriter = new VolatileField(field, networkManager, true);
@SuppressWarnings("unchecked")
List<Packet> minecraftList = (List<Packet>) overwriter.getOldValue();
synchronized(syncObject) {
// The list we'll be inserting
List<Packet> hackedList = new InjectedArrayList(manager.getClassLoader(), this, ignoredPackets);
// Add every previously stored packet
for (Packet packet : minecraftList) {
hackedList.add(packet);
}
// Don' keep stale packets around
minecraftList.clear();
overwriter.setValue(Collections.synchronizedList(hackedList));
}
overridenLists.add(overwriter);
}
}
}
@SuppressWarnings("unchecked")
public void cleanupAll() {
// Clean up
for (VolatileField overriden : overridenLists) {
List<Packet> minecraftList = (List<Packet>) overriden.getOldValue();
List<Packet> hacketList = (List<Packet>) overriden.getValue();
if (minecraftList == hacketList) {
return;
}
// Get a lock before we modify the list
synchronized(syncObject) {
try {
// Copy over current packets
for (Packet packet : (List<Packet>) overriden.getValue()) {
minecraftList.add(packet);
}
} finally {
overriden.revertValue();
}
}
}
overridenLists.clear();
}
}
@@ -0,0 +1,97 @@
package com.comphenix.protocol.injector;
import java.lang.reflect.InvocationTargetException;
import net.minecraft.server.Packet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.util.Set;
import org.bukkit.entity.Player;
/**
* Injection method that overrides the NetworkHandler itself, and it's sendPacket-method.
*
* @author Kristian
*/
class NetworkObjectInjector extends PlayerInjector {
public NetworkObjectInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
super(player, manager, sendingFilters);
}
@Override
public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException {
Object networkDelegate = filtered ? networkManagerRef.getValue() : networkManagerRef.getOldValue();
if (networkDelegate != null) {
try {
// Note that invocation target exception is a wrapper for a checked exception
queueMethod.invoke(networkDelegate, packet);
} catch (IllegalArgumentException e) {
throw e;
} catch (InvocationTargetException e) {
throw e;
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unable to access queue method.", e);
}
} else {
throw new IllegalStateException("Unable to load network mananager. Cannot send packet.");
}
}
@Override
public void injectManager() {
if (networkManager != null) {
final Class<?> networkInterface = networkManagerField.getType();
final Object networkDelegate = networkManagerRef.getOldValue();
if (!networkInterface.isInterface()) {
throw new UnsupportedOperationException(
"Must use CraftBukkit 1.3.0 or later to inject into into NetworkMananger.");
}
// Create our proxy object
Object networkProxy = Proxy.newProxyInstance(networkInterface.getClassLoader(),
new Class<?>[] { networkInterface }, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// OH OH! The queue method!
if (method.equals(queueMethod)) {
Packet packet = (Packet) args[0];
if (packet != null) {
packet = handlePacketRecieved(packet);
// A NULL packet indicate cancelling
if (packet != null)
args[0] = packet;
else
return null;
}
}
// Delegate to our underlying class
try {
return method.invoke(networkDelegate, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
});
// Inject it, if we can.
networkManagerRef.setValue(networkProxy);
}
}
@Override
public void cleanupAll() {
// Clean up
networkManagerRef.revertValue();
}
}
@@ -0,0 +1,235 @@
package com.comphenix.protocol.injector;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.server.Packet;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
/**
* A packet constructor that uses an internal Minecraft.
* @author Kristian
*
*/
public class PacketConstructor {
/**
* A packet constructor that automatically converts Bukkit types to their NMS conterpart.
* <p>
* Remember to call withPacket().
*/
public static PacketConstructor DEFAULT = new PacketConstructor(null);
// The constructor method that's actually responsible for creating the packet
private Constructor<?> constructorMethod;
// The packet ID
private int packetID;
// Used to unwrap Bukkit objects
private List<Unwrapper> unwrappers;
private PacketConstructor(Constructor<?> constructorMethod) {
this.constructorMethod = constructorMethod;
this.unwrappers = Lists.newArrayList((Unwrapper) new BukkitUnwrapper());
}
private PacketConstructor(Constructor<?> constructorMethod, List<Unwrapper> unwrappers) {
this.constructorMethod = constructorMethod;
this.unwrappers = unwrappers;
}
public ImmutableList<Unwrapper> getUnwrappers() {
return ImmutableList.copyOf(unwrappers);
}
/**
* Retrieve the id of the packets this constructor creates.
* @return The ID of the packets this constructor will create.
*/
public int getPacketID() {
return packetID;
}
/**
* Return a copy of the current constructor with a different list of unwrappers.
* @param unwrappers - list of unwrappers that convert Bukkit wrappers into the equivalent NMS classes.
* @return A constructor with a different set of unwrappers.
*/
public PacketConstructor withUnwrappers(List<Unwrapper> unwrappers) {
return new PacketConstructor(constructorMethod, unwrappers);
}
/**
* Create a packet constructor that creates packets using the given types.
* @param id - packet ID.
* @param types - types to create.
* @return A packet constructor with these types.
* @throws IllegalArgumentException If no packet constructor could be created with these types.
*/
public PacketConstructor withPacket(int id, Class<?>[] types) {
for (int i = 0; i < types.length; i++) {
for (Unwrapper unwrapper : unwrappers) {
Class<?> result = unwrapper.unwrapType(types[i]);
// Update type we're searching for
if (result != null) {
types[i] = result;
break;
}
}
}
Class<?> packetType = MinecraftRegistry.getPacketClassFromID(id);
if (packetType == null)
throw new IllegalArgumentException("Could not find a packet by the id " + id);
// Find the correct constructor
for (Constructor<?> constructor : packetType.getConstructors()) {
Class<?>[] params = constructor.getParameterTypes();
if (isCompatible(types, params)) {
// Right, we've found our type
return new PacketConstructor(constructor, unwrappers);
}
}
throw new IllegalArgumentException("No suitable constructor could be found.");
}
/**
* Construct a packet using the special builtin Minecraft constructors.
* @param values - values containing Bukkit wrapped items to pass to Minecraft.
* @return The created packet.
* @throws FieldAccessException Failure due to a security limitation.
* @throws IllegalArgumentException Arguments doesn't match the constructor.
* @throws RuntimeException Minecraft threw an exception.
*/
public PacketContainer createPacket(Object... values) throws FieldAccessException {
try {
// Convert types
for (int i = 0; i < values.length; i++) {
for (Unwrapper unwrapper : unwrappers) {
Object converted = unwrapper.unwrapItem(values[i]);
if (converted != null) {
values[i] = converted;
break;
}
}
}
Packet nmsPacket = (Packet) constructorMethod.newInstance(values);
return new PacketContainer(packetID, nmsPacket);
} catch (IllegalArgumentException e) {
throw e;
} catch (InstantiationException e) {
throw new FieldAccessException("Cannot construct an abstract packet.", e);
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot construct packet due to a security limitation.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Minecraft error.", e);
}
}
// Determine if a method with the types 'params' can be called with 'types'
private static boolean isCompatible(Class<?>[] types, Class<?>[] params) {
// Determine if the types are similar
if (params.length == types.length) {
for (int i = 0; i < params.length; i++) {
if (!params[i].isAssignableFrom(types[i])) {
return false;
}
}
return true;
}
// Parameter count must match
return false;
}
public static class BukkitUnwrapper implements Unwrapper {
private static Map<Class<?>, Method> cache = new ConcurrentHashMap<Class<?>, Method>();
@Override
public Object unwrapItem(Object wrappedObject) {
Class<?> currentClass = wrappedObject.getClass();
Method cachedMethod = initializeCache(currentClass);
try {
// Retrieve the handle
if (cachedMethod != null)
return cachedMethod.invoke(wrappedObject);
else
return null;
} catch (IllegalArgumentException e) {
// Impossible
return null;
} catch (IllegalAccessException e) {
return null;
} catch (InvocationTargetException e) {
// This is REALLY bad
throw new RuntimeException("Minecraft error.", e);
}
}
@Override
public Class<?> unwrapType(Class<?> type) {
if (type == null)
throw new IllegalArgumentException("type cannot be null.");
Method unwrapped = initializeCache(type);
// Determine the unwrapped type
if (unwrapped != null)
return unwrapped.getReturnType();
else
return null;
}
private Method initializeCache(Class<?> type) {
// See if we're already determined this
if (cache.containsKey(type)) {
// We will never remove from the cache, so this ought to be thread safe
return cache.get(type);
}
try {
Method find = type.getMethod("getHandle");
// It's thread safe, as getMethod should return the same handle
cache.put(type, find);
return find;
} catch (SecurityException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
}
}
public static interface Unwrapper {
public Object unwrapItem(Object wrappedObject);
public Class<?> unwrapType(Class<?> type);
}
}
@@ -1,52 +1,102 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.io.DataInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.server.Packet;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.commons.lang.NullArgumentException;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.ConnectionSide;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.events.*;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
public final class PacketFilterManager implements ProtocolManager {
private Set<PacketListener> packetListeners = new CopyOnWriteArraySet<PacketListener>();
/**
* Sets the inject hook type. Different types allow for maximum compatibility.
* @author Kristian
*/
public enum PlayerInjectHooks {
/**
* Override the packet queue lists in NetworkHandler.
*/
NETWORK_HANDLER_FIELDS,
/**
* Override the network handler object itself.
*/
NETWORK_MANAGER_OBJECT
}
// Create a concurrent set
private Set<PacketListener> packetListeners =
Collections.newSetFromMap(new ConcurrentHashMap<PacketListener, Boolean>());
// Player injection
private Map<DataInputStream, Player> connectionLookup = new HashMap<DataInputStream, Player>();
private Map<DataInputStream, Player> connectionLookup = new ConcurrentHashMap<DataInputStream, Player>();
private Map<Player, PlayerInjector> playerInjection = new HashMap<Player, PlayerInjector>();
// Player injection type
private PlayerInjectHooks playerHook = PlayerInjectHooks.NETWORK_HANDLER_FIELDS;
// Packet injection
private PacketInjector packetInjector;
// Enabled packet filters
private Set<Integer> packetFilters = new HashSet<Integer>();
private Set<Integer> sendingFilters = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
// The two listener containers
private ConcurrentListenerMultimap recievedListeners = new ConcurrentListenerMultimap();
private ConcurrentListenerMultimap sendingListeners = new ConcurrentListenerMultimap();
// Whether or not this class has been closed
private boolean hasClosed;
// The default class loader
private ClassLoader classLoader;
// Error logger
private Logger logger;
@@ -55,12 +105,13 @@ public final class PacketFilterManager implements ProtocolManager {
*/
public PacketFilterManager(ClassLoader classLoader, Logger logger) {
if (logger == null)
throw new NullArgumentException("logger");
throw new IllegalArgumentException("logger cannot be NULL.");
if (classLoader == null)
throw new NullArgumentException("classLoader");
throw new IllegalArgumentException("classLoader cannot be NULL.");
try {
// Initialize values
this.classLoader = classLoader;
this.logger = logger;
this.packetInjector = new PacketInjector(classLoader, this, connectionLookup);
} catch (IllegalAccessException e) {
@@ -68,6 +119,22 @@ public final class PacketFilterManager implements ProtocolManager {
}
}
/**
* Retrieves how the server packets are read.
* @return Injection method for reading server packets.
*/
public PlayerInjectHooks getPlayerHook() {
return playerHook;
}
/**
* Sets how the server packets are read.
* @param playerHook - the new injection method for reading server packets.
*/
public void setPlayerHook(PlayerInjectHooks playerHook) {
this.playerHook = playerHook;
}
public Logger getLogger() {
return logger;
}
@@ -80,21 +147,71 @@ public final class PacketFilterManager implements ProtocolManager {
@Override
public void addPacketListener(PacketListener listener) {
if (listener == null)
throw new NullArgumentException("listener");
throw new IllegalArgumentException("listener cannot be NULL.");
packetListeners.add(listener);
enablePacketFilters(listener.getConnectionSide(),
listener.getPacketsID());
// A listener can only be added once
if (packetListeners.contains(listener))
return;
ListeningWhitelist sending = listener.getSendingWhitelist();
ListeningWhitelist receiving = listener.getReceivingWhitelist();
boolean hasSending = sending != null && sending.isEnabled();
boolean hasReceiving = receiving != null && receiving.isEnabled();
if (hasSending || hasReceiving) {
// Add listeners and hooks
if (hasSending) {
sendingListeners.addListener(listener, sending);
enablePacketFilters(ConnectionSide.SERVER_SIDE, sending.getWhitelist());
}
if (hasReceiving) {
recievedListeners.addListener(listener, receiving);
enablePacketFilters(ConnectionSide.CLIENT_SIDE, receiving.getWhitelist());
}
// Inform our injected hooks
packetListeners.add(listener);
}
}
@Override
public void removePacketListener(PacketListener listener) {
if (listener == null)
throw new NullArgumentException("listener");
throw new IllegalArgumentException("listener cannot be NULL");
List<Integer> sendingRemoved = null;
List<Integer> receivingRemoved = null;
packetListeners.remove(listener);
disablePacketFilters(listener.getConnectionSide(),
listener.getPacketsID());
ListeningWhitelist sending = listener.getSendingWhitelist();
ListeningWhitelist receiving = listener.getReceivingWhitelist();
// Remove from the overal list of listeners
if (!packetListeners.remove(listener))
return;
// Add listeners
if (sending != null && sending.isEnabled())
sendingRemoved = sendingListeners.removeListener(listener, sending);
if (receiving != null && receiving.isEnabled())
receivingRemoved = recievedListeners.removeListener(listener, receiving);
// Remove hooks, if needed
if (sendingRemoved != null && sendingRemoved.size() > 0)
disablePacketFilters(ConnectionSide.SERVER_SIDE, sendingRemoved);
if (receivingRemoved != null && receivingRemoved.size() > 0)
disablePacketFilters(ConnectionSide.CLIENT_SIDE, receivingRemoved);
}
@Override
public void removePacketListeners(Plugin plugin) {
// Iterate through every packet listener
for (PacketListener listener : packetListeners) {
// Remove the listener
if (Objects.equal(listener.getPlugin(), plugin)) {
removePacketListener(listener);
}
}
}
/**
@@ -102,15 +219,7 @@ public final class PacketFilterManager implements ProtocolManager {
* @param event - the packet event to invoke.
*/
public void invokePacketRecieving(PacketEvent event) {
for (PacketListener listener : packetListeners) {
try {
if (canHandlePacket(listener, event))
listener.onPacketReceiving(event);
} catch (Exception e) {
// Minecraft doesn't want your Exception.
logger.log(Level.SEVERE, "Exception occured in onPacketReceiving() for " + listener.toString(), e);
}
}
recievedListeners.invokePacketRecieving(logger, event);
}
/**
@@ -118,26 +227,7 @@ public final class PacketFilterManager implements ProtocolManager {
* @param event - the packet event to invoke.
*/
public void invokePacketSending(PacketEvent event) {
for (PacketListener listener : packetListeners) {
try {
if (canHandlePacket(listener, event))
listener.onPacketSending(event);
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception occured in onPacketReceiving() for " + listener.toString(), e);
}
}
}
private boolean canHandlePacket(PacketListener listener, PacketEvent event) {
// Make sure the listener is looking for this packet
if (!listener.getPacketsID().contains(event.getPacket().getID()))
return false;
// And this type of packet
if (event.isServerPacket())
return listener.getConnectionSide().isForServer();
else
return listener.getConnectionSide().isForClient();
sendingListeners.invokePacketSending(logger, event);
}
/**
@@ -148,13 +238,13 @@ public final class PacketFilterManager implements ProtocolManager {
* @param side - which side the event will arrive from.
* @param packets - the packet id(s).
*/
private void enablePacketFilters(ConnectionSide side, Set<Integer> packets) {
private void enablePacketFilters(ConnectionSide side, Iterable<Integer> packets) {
if (side == null)
throw new NullArgumentException("side");
throw new IllegalArgumentException("side cannot be NULL.");
for (int packetID : packets) {
if (side.isForServer())
packetFilters.add(packetID);
if (side.isForServer())
sendingFilters.add(packetID);
if (side.isForClient() && packetInjector != null)
packetInjector.addPacketHandler(packetID);
}
@@ -165,13 +255,13 @@ public final class PacketFilterManager implements ProtocolManager {
* @param packets - the packet id(s).
* @param side - which side the event no longer should arrive from.
*/
private void disablePacketFilters(ConnectionSide side, Set<Integer> packets) {
private void disablePacketFilters(ConnectionSide side, Iterable<Integer> packets) {
if (side == null)
throw new NullArgumentException("side");
throw new IllegalArgumentException("side cannot be NULL.");
for (int packetID : packets) {
if (side.isForServer())
packetFilters.remove(packetID);
sendingFilters.remove(packetID);
if (side.isForClient() && packetInjector != null)
packetInjector.removePacketHandler(packetID);
}
@@ -185,9 +275,9 @@ public final class PacketFilterManager implements ProtocolManager {
@Override
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
if (reciever == null)
throw new NullArgumentException("reciever");
throw new IllegalArgumentException("reciever cannot be NULL.");
if (packet == null)
throw new NullArgumentException("packet");
throw new IllegalArgumentException("packet cannot be NULL.");
getInjector(reciever).sendServerPacket(packet.getHandle(), filters);
}
@@ -201,9 +291,9 @@ public final class PacketFilterManager implements ProtocolManager {
public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException {
if (sender == null)
throw new NullArgumentException("sender");
throw new IllegalArgumentException("sender cannot be NULL.");
if (packet == null)
throw new NullArgumentException("packet");
throw new IllegalArgumentException("packet cannot be NULL.");
PlayerInjector injector = getInjector(sender);
Packet mcPacket = packet.getHandle();
@@ -217,15 +307,50 @@ public final class PacketFilterManager implements ProtocolManager {
@Override
public PacketContainer createPacket(int id) {
return new PacketContainer(id);
return createPacket(id, true);
}
@Override
public Set<Integer> getPacketFilters() {
if (packetInjector != null)
return Sets.union(packetFilters, packetInjector.getPacketHandlers());
else
return packetFilters;
public PacketContainer createPacket(int id, boolean forceDefaults) {
PacketContainer packet = new PacketContainer(id);
// Use any default values if possible
if (forceDefaults) {
try {
packet.getModifier().writeDefaults();
} catch (FieldAccessException e) {
throw new RuntimeException("Security exception.", e);
}
}
return packet;
}
@Override
public PacketConstructor createPacketConstructor(int id, Object... arguments) {
Class<?>[] types = new Class<?>[arguments.length];
// Initialize types
for (int i = 0; i < arguments.length; i++) {
types[i] = arguments[i] != null ? arguments[i].getClass() : Object.class;
}
return PacketConstructor.DEFAULT.withPacket(id, types);
}
@Override
public Set<Integer> getSendingFilters() {
return ImmutableSet.copyOf(sendingFilters);
}
@Override
public Set<Integer> getReceivingFilters() {
return ImmutableSet.copyOf(packetInjector.getPacketHandlers());
}
@Override
public void updateEntity(Entity entity, List<Player> observers) throws FieldAccessException {
EntityUtilities.updateEntity(entity, observers);
}
/**
@@ -237,12 +362,35 @@ public final class PacketFilterManager implements ProtocolManager {
injectPlayer(player);
}
private void injectPlayer(Player player) {
/**
* Used to construct a player hook.
* @param player - the player to hook.
* @return A new player hoook
* @throws IllegalAccessException Unable to do our reflection magic.
*/
protected PlayerInjector getPlayerHookInstance(Player player) throws IllegalAccessException {
// Construct the correct player hook
switch (playerHook) {
case NETWORK_HANDLER_FIELDS:
return new NetworkFieldInjector(player, this, sendingFilters);
case NETWORK_MANAGER_OBJECT:
return new NetworkObjectInjector(player, this, sendingFilters);
}
throw new IllegalArgumentException("Cannot construct a player injector.");
}
/**
* Initialize a player hook, allowing us to read server packets.
* @param player - player to hook.
*/
protected void injectPlayer(Player player) {
// Don't inject if the class has closed
if (!hasClosed && player != null && !playerInjection.containsKey(player)) {
try {
PlayerInjector injector = new PlayerInjector(player, this, packetFilters);
PlayerInjector injector = getPlayerHookInstance(player);
injector.injectManager();
playerInjection.put(player, injector);
connectionLookup.put(injector.getInputStream(false), player);
@@ -260,19 +408,121 @@ public final class PacketFilterManager implements ProtocolManager {
* @param manager - Bukkit plugin manager that provides player join/leave events.
* @param plugin - the parent plugin.
*/
public void registerEvents(PluginManager manager, Plugin plugin) {
manager.registerEvents(new Listener() {
public void registerEvents(PluginManager manager, final Plugin plugin) {
try {
manager.registerEvents(new Listener() {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
injectPlayer(event.getPlayer());
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
uninjectPlayer(event.getPlayer());
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPluginDisabled(PluginDisableEvent event) {
// Clean up in case the plugin forgets
if (event.getPlugin() != plugin) {
removePacketListeners(event.getPlugin());
}
}
}, plugin);
} catch (NoSuchMethodError e) {
// Oh wow! We're running on 1.0.0 or older.
registerOld(manager, plugin);
}
}
// Yes, this is crazy.
@SuppressWarnings({ "unchecked", "rawtypes" })
private void registerOld(PluginManager manager, Plugin plugin) {
try {
ClassLoader loader = manager.getClass().getClassLoader();
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
injectPlayer(event.getPlayer());
}
// The different enums we are going to need
Class eventTypes = loader.loadClass("org.bukkit.event.Event$Type");
Class eventPriority = loader.loadClass("org.bukkit.event.Event$Priority");
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
uninjectPlayer(event.getPlayer());
}
}, plugin);
// Get the priority
Object priorityNormal = Enum.valueOf(eventPriority, "Normal");
// Get event types
Object playerJoinType = Enum.valueOf(eventTypes, "PLAYER_JOIN");
Object playerQuitType = Enum.valueOf(eventTypes, "PLAYER_QUIT");
Object pluginDisabledType = Enum.valueOf(eventTypes, "PLUGIN_DISABLE");
// The player listener! Good times.
Class<?> playerListener = loader.loadClass("org.bukkit.event.player.PlayerListener");
Class<?> serverListener = loader.loadClass("org.bukkit.event.server.ServerListener");
// Find the register event method
Method registerEvent = FuzzyReflection.fromObject(manager).getMethodByParameters("registerEvent",
eventTypes, Listener.class, eventPriority, Plugin.class);
Enhancer playerEx = new Enhancer();
Enhancer serverEx = new Enhancer();
playerEx.setSuperclass(playerListener);
playerEx.setClassLoader(classLoader);
playerEx.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// Must have a parameter
if (args.length == 1) {
Object event = args[0];
// Check for the correct event
if (event instanceof PlayerJoinEvent)
injectPlayer(((PlayerJoinEvent) event).getPlayer());
else if (event instanceof PlayerQuitEvent)
injectPlayer(((PlayerQuitEvent) event).getPlayer());
}
return null;
}
});
serverEx.setSuperclass(serverListener);
serverEx.setClassLoader(classLoader);
serverEx.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
// Must have a parameter
if (args.length == 1) {
Object event = args[0];
if (event instanceof PluginDisableEvent)
removePacketListeners(((PluginDisableEvent) event).getPlugin());
}
return null;
}
});
// Create our listener
Object playerProxy = playerEx.create();
Object serverProxy = serverEx.create();
registerEvent.invoke(manager, playerJoinType, playerProxy, priorityNormal, plugin);
registerEvent.invoke(manager, playerQuitType, playerProxy, priorityNormal, plugin);
registerEvent.invoke(manager, pluginDisabledType, serverProxy, priorityNormal, plugin);
// A lot can go wrong
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private void uninjectPlayer(Player player) {
@@ -305,6 +555,14 @@ public final class PacketFilterManager implements ProtocolManager {
throw new IllegalArgumentException("Player has no injected handler.");
}
/**
* Retrieves the current plugin class loader.
* @return Class loader.
*/
public ClassLoader getClassLoader() {
return classLoader;
}
@Override
public boolean isClosed() {
return hasClosed;
@@ -324,6 +582,8 @@ public final class PacketFilterManager implements ProtocolManager {
if (packetInjector != null)
packetInjector.cleanupAll();
// Remove listeners
packetListeners.clear();
playerInjection.clear();
connectionLookup.clear();
hasClosed = true;
@@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.io.DataInputStream;
@@ -15,6 +32,7 @@ import net.sf.cglib.proxy.Enhancer;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
/**
@@ -49,10 +67,10 @@ class PacketInjector {
private void initialize() throws IllegalAccessException {
if (intHashMap == null) {
// We're looking for the first static field with a Minecraft-object. This should be a IntHashMap.
Field intHashMapField = FuzzyReflection.fromClass(Packet.class).getFieldByType(FuzzyReflection.MINECRAFT_OBJECT);
Field intHashMapField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType(FuzzyReflection.MINECRAFT_OBJECT);
try {
intHashMap = intHashMapField.get(null);
intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Minecraft is incompatible.", e);
}
@@ -1,11 +1,26 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.io.DataInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Set;
import net.minecraft.server.EntityPlayer;
@@ -18,48 +33,55 @@ import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.VolatileField;
class PlayerInjector {
abstract class PlayerInjector {
// Cache previously retrieved fields
private static Field serverHandlerField;
private static Field networkManagerField;
private static Field inputField;
private static Field netHandlerField;
protected static Field serverHandlerField;
protected static Field networkManagerField;
protected static Field inputField;
protected static Field netHandlerField;
// To add our injected array lists
protected static StructureModifier<Object> networkModifier;
// And methods
private static Method queueMethod;
private static Method processMethod;
protected static Method queueMethod;
protected static Method processMethod;
private Player player;
private boolean hasInitialized;
protected Player player;
protected boolean hasInitialized;
// Reference to the player's network manager
private VolatileField networkManager;
// The packet manager and filters
private PacketFilterManager manager;
private Set<Integer> packetFilters;
// Previous data input
private DataInputStream cachedInput;
protected VolatileField networkManagerRef;
protected Object networkManager;
// Current net handler
private Object netHandler;
protected Object netHandler;
// The packet manager and filters
protected PacketFilterManager manager;
protected Set<Integer> sendingFilters;
// Previous data input
protected DataInputStream cachedInput;
public PlayerInjector(Player player, PacketFilterManager manager, Set<Integer> packetFilters) throws IllegalAccessException {
public PlayerInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
this.player = player;
this.manager = manager;
this.packetFilters = packetFilters;
this.sendingFilters = sendingFilters;
initialize();
}
private void initialize() throws IllegalAccessException {
protected void initialize() throws IllegalAccessException {
CraftPlayer craft = (CraftPlayer) player;
EntityPlayer notchEntity = craft.getHandle();
Object serverHandler = null;
if (!hasInitialized) {
// Do this first, in case we encounter an exception
hasInitialized = true;
@@ -67,12 +89,17 @@ class PlayerInjector {
// Retrieve the server handler
if (serverHandlerField == null)
serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType(".*NetServerHandler");
Object serverHandler = FieldUtils.readField(serverHandlerField, notchEntity);
serverHandler = FieldUtils.readField(serverHandlerField, notchEntity);
// Next, get the network manager
if (networkManagerField == null)
if (networkManagerField == null)
networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType(".*NetworkManager");
networkManager = new VolatileField(networkManagerField, serverHandler);
networkManagerRef = new VolatileField(networkManagerField, serverHandler);
networkManager = networkManagerRef.getValue();
// Create the network manager modifier from the actual object type
if (networkManager != null && networkModifier == null)
networkModifier = new StructureModifier<Object>(networkManager.getClass(), null, false);
// And the queue method
if (queueMethod == null)
@@ -81,7 +108,7 @@ class PlayerInjector {
// And the data input stream that we'll use to identify a player
if (inputField == null)
inputField = FuzzyReflection.fromObject(networkManager.getOldValue(), true).
inputField = FuzzyReflection.fromObject(networkManager, true).
getFieldByType("java\\.io\\.DataInputStream");
}
}
@@ -111,7 +138,7 @@ class PlayerInjector {
// Get the handler
if (netHandler != null)
netHandler = FieldUtils.readField(netHandlerField, networkManager.getOldValue(), true);
netHandler = FieldUtils.readField(netHandlerField, networkManager, true);
return netHandler;
}
@@ -151,66 +178,17 @@ class PlayerInjector {
* @param filtered - whether or not the packet will be filtered by our listeners.
* @param InvocationTargetException If an error occured when sending the packet.
*/
public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException {
Object networkDelegate = filtered ? networkManager.getValue() : networkManager.getOldValue();
if (networkDelegate != null) {
try {
// Note that invocation target exception is a wrapper for a checked exception
queueMethod.invoke(networkDelegate, packet);
} catch (IllegalArgumentException e) {
throw e;
} catch (InvocationTargetException e) {
throw e;
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unable to access queue method.", e);
}
} else {
throw new IllegalStateException("Unable to load network mananager. Cannot send packet.");
}
}
public abstract void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException;
public void injectManager() {
if (networkManager != null) {
final Class<?> networkInterface = networkManagerField.getType();
final Object networkDelegate = networkManager.getOldValue();
// Create our proxy object
Object networkProxy = Proxy.newProxyInstance(networkInterface.getClassLoader(),
new Class<?>[] { networkInterface }, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// OH OH! The queue method!
if (method.equals(queueMethod)) {
Packet packet = (Packet) args[0];
if (packet != null) {
packet = handlePacketRecieved(packet);
// A NULL packet indicate cancelling
if (packet != null)
args[0] = packet;
else
return null;
}
}
// Delegate to our underlying class
try {
return method.invoke(networkDelegate, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
});
// Inject it, if we can.
networkManager.setValue(networkProxy);
}
}
/**
* Inject a hook to catch packets sent to the current player.
*/
public abstract void injectManager();
/**
* Remove all hooks and modifications.
*/
public abstract void cleanupAll();
/**
* Allows a packet to be recieved by the listeners.
@@ -222,7 +200,7 @@ class PlayerInjector {
Integer id = MinecraftRegistry.getPacketToID().get(packet.getClass());
// Make sure we're listening
if (packetFilters.contains(id)) {
if (sendingFilters.contains(id)) {
// A packet has been sent guys!
PacketContainer container = new PacketContainer(id, packet);
PacketEvent event = PacketEvent.fromServer(manager, container, player);
@@ -239,6 +217,11 @@ class PlayerInjector {
return packet;
}
/**
* Retrieve the current player's input stream.
* @param cache - whether or not to cache the result of this method.
* @return The player's input stream.
*/
public DataInputStream getInputStream(boolean cache) {
// Get the associated input stream
try {
@@ -246,16 +229,11 @@ class PlayerInjector {
return cachedInput;
// Save to cache
cachedInput = (DataInputStream) FieldUtils.readField(inputField, networkManager.getOldValue(), true);
cachedInput = (DataInputStream) FieldUtils.readField(inputField, networkManager, true);
return cachedInput;
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to read input stream.", e);
}
}
public void cleanupAll() {
// Clean up
networkManager.revertValue();
}
}
@@ -1,11 +1,27 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.io.DataInputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.WeakHashMap;
import org.apache.commons.lang.ArrayUtils;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
@@ -55,7 +71,7 @@ class ReadPacketModifier implements MethodInterceptor {
// Is this a readPacketData method?
if (returnValue == null &&
ArrayUtils.isEquals(method.getParameterTypes(), parameters)) {
Arrays.equals(method.getParameterTypes(), parameters)) {
// We need this in order to get the correct player
DataInputStream input = (DataInputStream) args[0];
@@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
import java.util.HashMap;
@@ -7,6 +24,10 @@ import net.minecraft.server.Packet;
import com.comphenix.protocol.reflect.StructureModifier;
/**
* Caches structure modifiers.
* @author Kristian
*/
public class StructureCache {
// Structure modifiers
private static Map<Integer, StructureModifier<Object>> structureModifiers = new HashMap<Integer, StructureModifier<Object>>();
@@ -37,7 +58,9 @@ public class StructureCache {
// Use the vanilla class definition
if (result == null) {
result = new StructureModifier<Object>(MinecraftRegistry.getPacketClassFromID(id, true));
result = new StructureModifier<Object>(
MinecraftRegistry.getPacketClassFromID(id, true), Packet.class, true);
structureModifiers.put(id, result);
}
@@ -0,0 +1,648 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.metrics;
/*
* Copyright 2011 Tyler Blair. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and contributors and should not be interpreted as representing official policies,
* either expressed or implied, of anybody else.
*/
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* <p>
* The metrics class obtains data about a plugin and submits statistics about it to the metrics backend.
* </p>
* <p>
* Public methods provided by this class:
* </p>
* <code>
* Graph createGraph(String name); <br/>
* void addCustomData(Metrics.Plotter plotter); <br/>
* void start(); <br/>
* </code>
*/
class Metrics {
/**
* The current revision number
*/
private final static int REVISION = 5;
/**
* The base url of the metrics domain
*/
private static final String BASE_URL = "http://mcstats.org";
/**
* The url used to report a server's status
*/
private static final String REPORT_URL = "/report/%s";
/**
* The separator to use for custom data. This MUST NOT change unless you are hosting your own
* version of metrics and want to change it.
*/
private static final String CUSTOM_DATA_SEPARATOR = "~~";
/**
* Interval of time to ping (in minutes)
*/
private static final int PING_INTERVAL = 10;
/**
* The plugin this metrics submits for
*/
private final Plugin plugin;
/**
* All of the custom graphs to submit to metrics
*/
private final Set<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
/**
* The default graph, used for addCustomData when you don't want a specific graph
*/
private final Graph defaultGraph = new Graph("Default");
/**
* The plugin configuration file
*/
private final YamlConfiguration configuration;
/**
* The plugin configuration file
*/
private final File configurationFile;
/**
* Unique server id
*/
private final String guid;
/**
* Lock for synchronization
*/
private final Object optOutLock = new Object();
/**
* Id of the scheduled task
*/
private volatile int taskId = -1;
public Metrics(final Plugin plugin) throws IOException {
if (plugin == null) {
throw new IllegalArgumentException("Plugin cannot be null");
}
this.plugin = plugin;
// load the config
configurationFile = getConfigFile();
configuration = YamlConfiguration.loadConfiguration(configurationFile);
// add some defaults
configuration.addDefault("opt-out", false);
configuration.addDefault("guid", UUID.randomUUID().toString());
// Do we need to create the file?
if (configuration.get("guid", null) == null) {
configuration.options().header("http://mcstats.org").copyDefaults(true);
configuration.save(configurationFile);
}
// Load the guid then
guid = configuration.getString("guid");
}
/**
* Construct and create a Graph that can be used to separate specific plotters to their own graphs
* on the metrics website. Plotters can be added to the graph object returned.
*
* @param name The name of the graph
* @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given
*/
public Graph createGraph(final String name) {
if (name == null) {
throw new IllegalArgumentException("Graph name cannot be null");
}
// Construct the graph object
final Graph graph = new Graph(name);
// Now we can add our graph
graphs.add(graph);
// and return back
return graph;
}
/**
* Add a Graph object to Metrics that represents data for the plugin that should be sent to the backend
*
* @param graph The name of the graph
*/
public void addGraph(final Graph graph) {
if (graph == null) {
throw new IllegalArgumentException("Graph cannot be null");
}
graphs.add(graph);
}
/**
* Adds a custom data plotter to the default graph
*
* @param plotter The plotter to use to plot custom data
*/
public void addCustomData(final Plotter plotter) {
if (plotter == null) {
throw new IllegalArgumentException("Plotter cannot be null");
}
// Add the plotter to the graph o/
defaultGraph.addPlotter(plotter);
// Ensure the default graph is included in the submitted graphs
graphs.add(defaultGraph);
}
/**
* Start measuring statistics. This will immediately create an async repeating task as the plugin and send
* the initial data to the metrics backend, and then after that it will post in increments of
* PING_INTERVAL * 1200 ticks.
*
* @return True if statistics measuring is running, otherwise false.
*/
public boolean start() {
synchronized (optOutLock) {
// Did we opt out?
if (isOptOut()) {
return false;
}
// Is metrics already running?
if (taskId >= 0) {
return true;
}
// Begin hitting the server with glorious data
taskId = plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() {
private boolean firstPost = true;
public void run() {
try {
// This has to be synchronized or it can collide with the disable method.
synchronized (optOutLock) {
// Disable Task, if it is running and the server owner decided to opt-out
if (isOptOut() && taskId > 0) {
plugin.getServer().getScheduler().cancelTask(taskId);
taskId = -1;
// Tell all plotters to stop gathering information.
for (Graph graph : graphs){
graph.onOptOut();
}
}
}
// We use the inverse of firstPost because if it is the first time we are posting,
// it is not a interval ping, so it evaluates to FALSE
// Each time thereafter it will evaluate to TRUE, i.e PING!
postPlugin(!firstPost);
// After the first post we set firstPost to false
// Each post thereafter will be a ping
firstPost = false;
} catch (IOException e) {
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage());
}
}
}, 0, PING_INTERVAL * 1200);
return true;
}
}
/**
* Has the server owner denied plugin metrics?
*
* @return true if metrics should be opted out of it
*/
public boolean isOptOut() {
synchronized(optOutLock) {
try {
// Reload the metrics file
configuration.load(getConfigFile());
} catch (IOException ex) {
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
return true;
} catch (InvalidConfigurationException ex) {
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
return true;
}
return configuration.getBoolean("opt-out", false);
}
}
/**
* Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task.
*
* @throws IOException
*/
public void enable() throws IOException {
// This has to be synchronized or it can collide with the check in the task.
synchronized (optOutLock) {
// Check if the server owner has already set opt-out, if not, set it.
if (isOptOut()) {
configuration.set("opt-out", false);
configuration.save(configurationFile);
}
// Enable Task, if it is not running
if (taskId < 0) {
start();
}
}
}
/**
* Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task.
*
* @throws IOException
*/
public void disable() throws IOException {
// This has to be synchronized or it can collide with the check in the task.
synchronized (optOutLock) {
// Check if the server owner has already set opt-out, if not, set it.
if (!isOptOut()) {
configuration.set("opt-out", true);
configuration.save(configurationFile);
}
// Disable Task, if it is running
if (taskId > 0) {
this.plugin.getServer().getScheduler().cancelTask(taskId);
taskId = -1;
}
}
}
/**
* Gets the File object of the config file that should be used to store data such as the GUID and opt-out status
*
* @return the File object for the config file
*/
public File getConfigFile() {
// I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use
// is to abuse the plugin object we already have
// plugin.getDataFolder() => base/plugins/PluginA/
// pluginsFolder => base/plugins/
// The base is not necessarily relative to the startup directory.
File pluginsFolder = plugin.getDataFolder().getParentFile();
// return => base/plugins/PluginMetrics/config.yml
return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml");
}
/**
* Generic method that posts a plugin to the metrics website
*/
private void postPlugin(final boolean isPing) throws IOException {
// The plugin's description file containg all of the plugin data such as name, version, author, etc
final PluginDescriptionFile description = plugin.getDescription();
// Construct the post data
final StringBuilder data = new StringBuilder();
data.append(encode("guid")).append('=').append(encode(guid));
encodeDataPair(data, "version", description.getVersion());
encodeDataPair(data, "server", Bukkit.getVersion());
encodeDataPair(data, "players", Integer.toString(Bukkit.getServer().getOnlinePlayers().length));
encodeDataPair(data, "revision", String.valueOf(REVISION));
// If we're pinging, append it
if (isPing) {
encodeDataPair(data, "ping", "true");
}
// Acquire a lock on the graphs, which lets us make the assumption we also lock everything
// inside of the graph (e.g plotters)
synchronized (graphs) {
final Iterator<Graph> iter = graphs.iterator();
while (iter.hasNext()) {
final Graph graph = iter.next();
for (Plotter plotter : graph.getPlotters()) {
// The key name to send to the metrics server
// The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top
// Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME
final String key = String.format("C%s%s%s%s", CUSTOM_DATA_SEPARATOR, graph.getName(), CUSTOM_DATA_SEPARATOR, plotter.getColumnName());
// The value to send, which for the foreseeable future is just the string
// value of plotter.getValue()
final String value = Integer.toString(plotter.getValue());
// Add it to the http post data :)
encodeDataPair(data, key, value);
}
}
}
// Create the url
URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName())));
// Connect to the website
URLConnection connection;
// Mineshafter creates a socks proxy, so we can safely bypass it
// It does not reroute POST requests so we need to go around it
if (isMineshafterPresent()) {
connection = url.openConnection(Proxy.NO_PROXY);
} else {
connection = url.openConnection();
}
connection.setDoOutput(true);
// Write the data
final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
writer.write(data.toString());
writer.flush();
// Now read the response
final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
final String response = reader.readLine();
// close resources
writer.close();
reader.close();
if (response == null || response.startsWith("ERR")) {
throw new IOException(response); //Throw the exception
} else {
// Is this the first update this hour?
if (response.contains("OK This is your first update this hour")) {
synchronized (graphs) {
final Iterator<Graph> iter = graphs.iterator();
while (iter.hasNext()) {
final Graph graph = iter.next();
for (Plotter plotter : graph.getPlotters()) {
plotter.reset();
}
}
}
}
}
}
/**
* Check if mineshafter is present. If it is, we need to bypass it to send POST requests
*
* @return true if mineshafter is installed on the server
*/
private boolean isMineshafterPresent() {
try {
Class.forName("mineshafter.MineServer");
return true;
} catch (Exception e) {
return false;
}
}
/**
* <p>Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first
* key/value pair MUST be included manually, e.g:</p>
* <code>
* StringBuffer data = new StringBuffer();
* data.append(encode("guid")).append('=').append(encode(guid));
* encodeDataPair(data, "version", description.getVersion());
* </code>
*
* @param buffer the stringbuilder to append the data pair onto
* @param key the key value
* @param value the value
*/
private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException {
buffer.append('&').append(encode(key)).append('=').append(encode(value));
}
/**
* Encode text as UTF-8
*
* @param text the text to encode
* @return the encoded text, as UTF-8
*/
private static String encode(final String text) throws UnsupportedEncodingException {
return URLEncoder.encode(text, "UTF-8");
}
/**
* Represents a custom graph on the website
*/
public static class Graph {
/**
* The graph's name, alphanumeric and spaces only :)
* If it does not comply to the above when submitted, it is rejected
*/
private final String name;
/**
* The set of plotters that are contained within this graph
*/
private final Set<Plotter> plotters = new LinkedHashSet<Plotter>();
private Graph(final String name) {
this.name = name;
}
/**
* Gets the graph's name
*
* @return the Graph's name
*/
public String getName() {
return name;
}
/**
* Add a plotter to the graph, which will be used to plot entries
*
* @param plotter the plotter to add to the graph
*/
public void addPlotter(final Plotter plotter) {
plotters.add(plotter);
}
/**
* Remove a plotter from the graph
*
* @param plotter the plotter to remove from the graph
*/
public void removePlotter(final Plotter plotter) {
plotters.remove(plotter);
}
/**
* Gets an <b>unmodifiable</b> set of the plotter objects in the graph
*
* @return an unmodifiable {@link Set} of the plotter objects
*/
public Set<Plotter> getPlotters() {
return Collections.unmodifiableSet(plotters);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(final Object object) {
if (!(object instanceof Graph)) {
return false;
}
final Graph graph = (Graph) object;
return graph.name.equals(name);
}
/**
* Called when the server owner decides to opt-out of Metrics while the server is running.
*/
protected void onOptOut() {
}
}
/**
* Interface used to collect custom data for a plugin
*/
public static abstract class Plotter {
/**
* The plot's name
*/
private final String name;
/**
* Construct a plotter with the default plot name
*/
public Plotter() {
this("Default");
}
/**
* Construct a plotter with a specific plot name
*
* @param name the name of the plotter to use, which will show up on the website
*/
public Plotter(final String name) {
this.name = name;
}
/**
* Get the current value for the plotted point. Since this function defers to an external function
* it may or may not return immediately thus cannot be guaranteed to be thread friendly or safe.
* This function can be called from any thread so care should be taken when accessing resources
* that need to be synchronized.
*
* @return the current value for the point to be plotted.
*/
public abstract int getValue();
/**
* Get the column name for the plotted point
*
* @return the plotted point's column name
*/
public String getColumnName() {
return name;
}
/**
* Called after the website graphs have been updated
*/
public void reset() {
}
@Override
public int hashCode() {
return getColumnName().hashCode();
}
@Override
public boolean equals(final Object object) {
if (!(object instanceof Plotter)) {
return false;
}
final Plotter plotter = (Plotter) object;
return plotter.name.equals(name) && plotter.getValue() == getValue();
}
}
}
@@ -0,0 +1,81 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.metrics;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.plugin.Plugin;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.metrics.Metrics.Graph;
public class Statistics {
// Metrics
private Metrics metrics;
public Statistics(Plugin plugin) throws IOException {
metrics = new Metrics(plugin);
metrics.start();
// Determine who is using this library
addPluginUserGraph(metrics);
}
private void addPluginUserGraph(Metrics metrics) {
Graph pluginUsers = metrics.createGraph("Plugin Users");
for (Map.Entry<String, Integer> entry : getPluginUsers(ProtocolLibrary.getProtocolManager()).entrySet()) {
final int count = entry.getValue();
// Plot plugins of this type
pluginUsers.addPlotter(new Metrics.Plotter(entry.getKey()) {
@Override
public int getValue() {
return count;
}
});
}
}
// Retrieve loaded plugins
private Map<String, Integer> getPluginUsers(ProtocolManager manager) {
Map<String, Integer> users = new HashMap<String, Integer>();
for (PacketListener listener : manager.getPacketListeners()) {
String name = PacketAdapter.getPluginName(listener);
// Increment occurence
if (!users.containsKey(name)) {
users.put(name, 1);
} else {
users.put(name, users.get(name) + 1);
}
}
return users;
}
}
@@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect;
/**
@@ -9,4 +26,5 @@ package com.comphenix.protocol.reflect;
public interface EquivalentConverter<TType> {
public TType getSpecific(Object generic);
public Object getGeneric(TType specific);
public Class<TType> getSpecificType();
}
@@ -0,0 +1,51 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect;
/**
* Invoked when a field is inaccessible due to security limitations, or when it simply doesn't exist.
*
* @author Kristian
*/
public class FieldAccessException extends Exception {
/**
* Generated by Eclipse.
*/
private static final long serialVersionUID = 1911011681494034617L;
public FieldAccessException() {
super();
}
public FieldAccessException(String message, Throwable cause) {
super(message, cause);
}
public FieldAccessException(String message) {
super(message);
}
public FieldAccessException(Throwable cause) {
super(cause);
}
public static FieldAccessException fromFormat(String message, Object... params) {
return new FieldAccessException(String.format(message, params));
}
}
@@ -21,9 +21,9 @@ import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.commons.lang.ClassUtils;
import java.util.List;
/**
* Utilities for working with fields by reflection. Adapted and refactored from
@@ -119,11 +119,11 @@ public class FieldUtils {
}
}
// check the public interface case. This must be manually searched for
// incase there is a public supersuperclass field hidden by a
// in case there is a public supersuperclass field hidden by a
// private/package
// superclass field.
Field match = null;
for (Iterator intf = ClassUtils.getAllInterfaces(cls).iterator(); intf.hasNext();) {
for (Iterator intf = getAllInterfaces(cls).iterator(); intf.hasNext();) {
try {
Field test = ((Class) intf.next()).getField(fieldName);
if (match != null) {
@@ -138,6 +138,44 @@ public class FieldUtils {
}
return match;
}
/**
* <p>Gets a <code>List</code> of all interfaces implemented by the given
* class and its superclasses.</p>
*
* <p>The order is determined by looking through each interface in turn as
* declared in the source file and following its hierarchy up. Then each
* superclass is considered in the same way. Later duplicates are ignored,
* so the order is maintained.</p>
*
* @param cls the class to look up, may be <code>null</code>
* @return the <code>List</code> of interfaces in order,
* <code>null</code> if null input
*/
private static List getAllInterfaces(Class cls) {
if (cls == null) {
return null;
}
List<Class> list = new ArrayList<Class>();
while (cls != null) {
Class[] interfaces = cls.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
if (list.contains(interfaces[i]) == false) {
list.add(interfaces[i]);
}
List superInterfaces = getAllInterfaces(interfaces[i]);
for (Iterator it = superInterfaces.iterator(); it.hasNext();) {
Class intface = (Class) it.next();
if (list.contains(intface) == false) {
list.add(intface);
}
}
}
cls = cls.getSuperclass();
}
return list;
}
/**
* Read an accessible static Field.
@@ -1,13 +1,29 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang.ArrayUtils;
/**
* Retrieves fields and methods by signature, not just name.
*
@@ -106,7 +122,27 @@ public class FuzzyReflection {
// Find the correct method to call
for (Method method : getMethods()) {
if (ArrayUtils.isEquals(method.getParameterTypes(), args)) {
if (Arrays.equals(method.getParameterTypes(), args)) {
return method;
}
}
// That sucks
throw new RuntimeException("Unable to find " + name + " in " + source.getName());
}
/**
* Retrieves a method by looking at the parameter types and return type only.
* @param name - potential name of the method. Only used by the error mechanism.
* @param returnType - return type of the method to find.
* @param args - parameter types of the method to find.
* @return The first method that satisfies the parameter types.
*/
public Method getMethodByParameters(String name, Class<?> returnType, Class<?>[] args) {
// Find the correct method to call
for (Method method : getMethods()) {
if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) {
return method;
}
}
@@ -163,6 +199,37 @@ public class FuzzyReflection {
typeRegex + " in " + source.getName());
}
/**
* Retrieves a field by type.
* <p>
* Note that the type is matched using the full canonical representation, i.e.:
* <ul>
* <li>java.util.List</li>
* <li>net.comphenix.xp.ExperienceMod</li>
* </ul>
* @param typeRegex - regular expression that will match the field type.
* @param ignored - types to ignore.
* @return The first field with a type that matches the given regular expression.
*/
@SuppressWarnings("rawtypes")
public Field getFieldByType(String typeRegex, Set<Class> ignored) {
Pattern match = Pattern.compile(typeRegex);
// Like above, only here we test the field type
for (Field field : getFields()) {
Class type = field.getType();
if (!ignored.contains(type) && match.matcher(type.getName()).matches()) {
return field;
}
}
// Looks like we're outdated. Too bad.
throw new RuntimeException("Unable to find a field with the type " +
typeRegex + " in " + source.getName());
}
/**
* Retrieves all private and public fields in declared order (after JDK 1.5).
* @return Every field.
@@ -0,0 +1,79 @@
package com.comphenix.protocol.reflect;
import java.lang.reflect.Field;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
/**
* Represents a traditional int field enum.
*
* @author Kristian
*/
public class IntEnum {
// Used to convert between IDs and names
protected BiMap<Integer, String> members = HashBiMap.create();
/**
* Registers every declared integer field.
*/
public IntEnum() {
registerAll();
}
/**
* Registers every public int field as a member.
*/
protected void registerAll() {
try {
// Register every int field
for (Field entry : this.getClass().getFields()) {
if (entry.getType().equals(int.class)) {
registerMember(entry.getInt(this), entry.getName());
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* Registers a member.
* @param id - id of member.
* @param name - name of member.
*/
protected void registerMember(int id, String name) {
members.put(id, name);
}
/**
* Determines whether or not the given member exists.
* @param id - the ID of the member to find.
* @return TRUE if a member with the given ID exists, FALSE otherwise.
*/
public boolean hasMember(int id) {
return members.containsKey(id);
}
/**
* Retrieve the ID of the member with the given name.
* @param name - name of member to retrieve.
* @return ID of the member, or NULL if not found.
*/
public Integer valueOf(String name) {
return members.inverse().get(name);
}
/**
* Retrieve the name of the member with the given id.
* @param id - id of the member to retrieve.
* @return Declared name of the member, or NULL if not found.
*/
public String getDeclaredName(Integer id) {
return members.get(id);
}
}
@@ -0,0 +1,124 @@
package com.comphenix.protocol.reflect;
/*
* Copyright (C) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Contains static utility methods pertaining to primitive types and their
* corresponding wrapper types.
*
* @author Kevin Bourrillion
*/
public final class PrimitiveUtils {
private PrimitiveUtils() {
}
/** A map from primitive types to their corresponding wrapper types. */
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE;
/** A map from wrapper types to their corresponding primitive types. */
private static final Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE_TYPE;
// Sad that we can't use a BiMap. :(
static {
Map<Class<?>, Class<?>> primToWrap = new HashMap<Class<?>, Class<?>>(16);
Map<Class<?>, Class<?>> wrapToPrim = new HashMap<Class<?>, Class<?>>(16);
add(primToWrap, wrapToPrim, boolean.class, Boolean.class);
add(primToWrap, wrapToPrim, byte.class, Byte.class);
add(primToWrap, wrapToPrim, char.class, Character.class);
add(primToWrap, wrapToPrim, double.class, Double.class);
add(primToWrap, wrapToPrim, float.class, Float.class);
add(primToWrap, wrapToPrim, int.class, Integer.class);
add(primToWrap, wrapToPrim, long.class, Long.class);
add(primToWrap, wrapToPrim, short.class, Short.class);
add(primToWrap, wrapToPrim, void.class, Void.class);
PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap);
WRAPPER_TO_PRIMITIVE_TYPE = Collections.unmodifiableMap(wrapToPrim);
}
private static void add(Map<Class<?>, Class<?>> forward,
Map<Class<?>, Class<?>> backward, Class<?> key, Class<?> value) {
forward.put(key, value);
backward.put(value, key);
}
/**
* Returns true if this type is a primitive.
*/
public static boolean isPrimitive(Type type) {
return PRIMITIVE_TO_WRAPPER_TYPE.containsKey(type);
}
/**
* Returns {@code true} if {@code type} is one of the nine primitive-wrapper
* types, such as {@link Integer}.
*
* @see Class#isPrimitive
*/
public static boolean isWrapperType(Type type) {
return WRAPPER_TO_PRIMITIVE_TYPE.containsKey(checkNotNull(type));
}
/**
* Returns the corresponding wrapper type of {@code type} if it is a
* primitive type; otherwise returns {@code type} itself. Idempotent.
*
* <pre>
* wrap(int.class) == Integer.class
* wrap(Integer.class) == Integer.class
* wrap(String.class) == String.class
* </pre>
*/
public static <T> Class<T> wrap(Class<T> type) {
// cast is safe: long.class and Long.class are both of type Class<Long>
@SuppressWarnings("unchecked")
Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE
.get(checkNotNull(type));
return (wrapped == null) ? type : wrapped;
}
/**
* Returns the corresponding primitive type of {@code type} if it is a
* wrapper type; otherwise returns {@code type} itself. Idempotent.
*
* <pre>
* unwrap(Integer.class) == int.class
* unwrap(int.class) == int.class
* unwrap(String.class) == String.class
* </pre>
*/
public static <T> Class<T> unwrap(Class<T> type) {
// cast is safe: long.class and Long.class are both of type Class<Long>
@SuppressWarnings("unchecked")
Class<T> unwrapped = (Class<T>) WRAPPER_TO_PRIMITIVE_TYPE
.get(checkNotNull(type));
return (unwrapped == null) ? type : unwrapped;
}
public static <T> T checkNotNull(T obj) {
if (obj == null) {
throw new NullPointerException();
}
return obj;
}
}
@@ -1,19 +1,38 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
import com.google.common.base.Function;
import net.minecraft.server.Packet;
import com.google.common.collect.ImmutableList;
@SuppressWarnings("rawtypes")
public class StructureModifier<TField> {
// Object and its type
private Class targetType;
private Object target;
@@ -25,47 +44,85 @@ public class StructureModifier<TField> {
private Class fieldType;
private List<Field> data = new ArrayList<Field>();
// Improved default values
private Set<Field> defaultFields;
// Cache of previous types
private Map<Class, StructureModifier> subtypeCache;
public StructureModifier(Class targetType) {
this(targetType, Object.class, getFields(targetType), null, new HashMap<Class, StructureModifier>());
}
private StructureModifier(Class targetType, Class fieldType, List<Field> data,
EquivalentConverter<TField> converter, Map<Class, StructureModifier> subTypeCache) {
this.targetType = targetType;
this.fieldType = fieldType;
this.data = data;
this.converter = converter;
this.subtypeCache = subTypeCache;
/**
* Creates a structure modifier.
* @param targetType - the structure to modify.
* @param superclassExclude - a superclass to exclude.
* @param requireDefault - whether or not we will be using writeDefaults().
*/
public StructureModifier(Class targetType, Class superclassExclude, boolean requireDefault) {
List<Field> fields = getFields(targetType, superclassExclude);
Set<Field> defaults = requireDefault ? generateDefaultFields(fields) : new HashSet<Field>();
initialize(targetType, Object.class, fields, defaults, null, new HashMap<Class, StructureModifier>());
}
private StructureModifier(StructureModifier<TField> other, Object target) {
this(other.targetType, other.fieldType, other.data, other.converter, other.subtypeCache);
initialize(other.targetType, other.fieldType, other.data, other.defaultFields, other.converter, other.subtypeCache);
this.target = target;
}
private StructureModifier() {
// Consumers of this method should call "initialize"
}
private void initialize(Class targetType, Class fieldType,
List<Field> data, Set<Field> defaultFields,
EquivalentConverter<TField> converter, Map<Class, StructureModifier> subTypeCache) {
this.targetType = targetType;
this.fieldType = fieldType;
this.data = data;
this.defaultFields = defaultFields;
this.converter = converter;
this.subtypeCache = subTypeCache;
}
/**
* Reads the value of a field given its index.
* @param fieldIndex - index of the field.
* @return Value of the field.
* @throws IllegalAccessException If we're unable to read the field due to a security limitation.
* @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints.
*/
@SuppressWarnings("unchecked")
public TField read(int fieldIndex) throws IllegalAccessException {
public TField read(int fieldIndex) throws FieldAccessException {
if (fieldIndex < 0 || fieldIndex >= data.size())
throw new IllegalArgumentException("Field index must be within 0 - count");
throw new FieldAccessException("Field index must be within 0 - count",
new IndexOutOfBoundsException("Out of bounds"));
if (target == null)
throw new IllegalStateException("Cannot read from a NULL target.");
Object result = FieldUtils.readField(data.get(fieldIndex), target, true);
// Use the converter, if we have it
if (converter != null)
return converter.getSpecific(result);
else
return (TField) result;
try {
Object result = FieldUtils.readField(data.get(fieldIndex), target, true);
// Use the converter, if we have it
if (converter != null)
return converter.getSpecific(result);
else
return (TField) result;
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot read field due to a security limitation.", e);
}
}
/**
* Reads the value of a field if and ONLY IF it exists.
* @param fieldIndex - index of the field.
* @return Value of the field, or NULL if it doesn't exist.
* @throws FieldAccessException The field cannot be accessed under the current security contraints.
*/
public TField readSafely(int fieldIndex) throws FieldAccessException {
if (fieldIndex >= 0 && fieldIndex < data.size()) {
return read(fieldIndex);
} else {
return null;
}
}
/**
@@ -73,30 +130,50 @@ public class StructureModifier<TField> {
* @param fieldIndex - index of the field.
* @param value - new value of the field.
* @return This structure modifier - for chaining.
* @throws IllegalAccessException If we're unable to write to the field due to a security limitation.
* @throws FieldAccessException The field doesn't exist, or it cannot be accessed under the current security contraints.
*/
public StructureModifier<TField> write(int fieldIndex, TField value) throws IllegalAccessException {
public StructureModifier<TField> write(int fieldIndex, TField value) throws FieldAccessException {
if (fieldIndex < 0 || fieldIndex >= data.size())
throw new IllegalArgumentException("Field index must be within 0 - count");
throw new FieldAccessException("Field index must be within 0 - count",
new IndexOutOfBoundsException("Out of bounds"));
if (target == null)
throw new IllegalStateException("Cannot write to a NULL target.");
// Use the converter, if it exists
Object obj = converter != null ? converter.getGeneric(value) : value;
FieldUtils.writeField(data.get(fieldIndex), target, obj, true);
try {
FieldUtils.writeField(data.get(fieldIndex), target, obj, true);
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot read field due to a security limitation.", e);
}
// Make this method chainable
return this;
}
/**
* Writes the value of a given field IF and ONLY if it exists.
* @param fieldIndex - index of the potential field.
* @param value - new value of the field.
* @return This structure modifer - for chaining.
* @throws FieldAccessException The field cannot be accessed under the current security contraints.
*/
public StructureModifier<TField> writeSafely(int fieldIndex, TField value) throws FieldAccessException {
if (fieldIndex >= 0 && fieldIndex < data.size()) {
write(fieldIndex, value);
}
return this;
}
/**
* Correctly modifies the value of a field.
* @param fieldIndex - index of the field to modify.
* @param select - the function that modifies the field value.
* @return This structure modifier - for chaining.
* @throws IllegalAccessException
* @throws FieldAccessException The field cannot be accessed under the current security contraints.
*/
public StructureModifier<TField> modify(int fieldIndex, Function<TField, TField> select) throws IllegalAccessException {
public StructureModifier<TField> modify(int fieldIndex, Function<TField, TField> select) throws FieldAccessException {
TField value = read(fieldIndex);
return write(fieldIndex, select.apply(value));
}
@@ -110,6 +187,28 @@ public class StructureModifier<TField> {
return withType(fieldType, null);
}
/**
* Sets all non-primitive fields to a more fitting default value. See {@link DefaultInstances#getDefault(Class)}.
* @return The current structure modifier - for chaining.
* @throws FieldAccessException If we're unable to write to the fields due to a security limitation.
*/
public StructureModifier<TField> writeDefaults() throws FieldAccessException {
DefaultInstances generator = DefaultInstances.DEFAULT;
// Write a default instance to every field
for (Field field : defaultFields) {
try {
FieldUtils.writeField(field, target,
generator.getDefault(field.getType()), true);
} catch (IllegalAccessException e) {
throw new FieldAccessException("Cannot write to field due to a security limitation.", e);
}
}
return this;
}
/**
* Retrieves a structure modifier that only reads and writes fields of a given type.
* @param fieldType - the type, or supertype, of every field to modify.
@@ -121,28 +220,48 @@ public class StructureModifier<TField> {
StructureModifier<T> result = subtypeCache.get(fieldType);
if (fieldType.equals(this.fieldType)) {
// We're dealing with the exact field type.
return withConverter(converter);
} else if (result == null) {
// Do we need to update the cache?
if (result == null) {
List<Field> filtered = new ArrayList<Field>();
Set<Field> defaults = new HashSet<Field>();
for (Field field : data) {
if (fieldType.isAssignableFrom(field.getType())) {
if (fieldType != null && fieldType.isAssignableFrom(field.getType())) {
filtered.add(field);
if (defaultFields.contains(field))
defaults.add(field);
}
}
// Cache structure modifiers
result = new StructureModifier<T>(targetType, fieldType, filtered,
converter, new HashMap<Class, StructureModifier>());
subtypeCache.put(fieldType, result);
result = new StructureModifier<T>();
result.initialize(targetType, fieldType, filtered, defaults,
converter, new HashMap<Class, StructureModifier>());
if (fieldType != null)
subtypeCache.put(fieldType, result);
}
// Add the target too
return result.withTarget(target);
result = result.withTarget(target);
// And the converter, if it's needed
if (!sameConverter(result.converter, converter)) {
result = result.withConverter(converter);
}
return result;
}
private boolean sameConverter(EquivalentConverter<?> a, EquivalentConverter<?> b) {
// Compare the converter types
if (a == null)
return b == null;
else if (b == null)
return a == null;
else
return a.getSpecificType().equals(b.getSpecificType());
}
/**
@@ -199,8 +318,52 @@ public class StructureModifier<TField> {
return copy;
}
/**
* Retrieves a list of the fields matching the constraints of this structure modifier.
* @return List of fields.
*/
public List<Field> getFields() {
return ImmutableList.copyOf(data);
}
/**
* Retrieve every value stored in the fields of the current type.
* @return Every field value.
* @throws FieldAccessException Unable to access one or all of the fields
*/
public List<TField> getValues() throws FieldAccessException {
List<TField> values = new ArrayList<TField>();
for (int i = 0; i < size(); i++)
values.add(read(i));
return values;
}
// Used to generate plausible default values
private static Set<Field> generateDefaultFields(List<Field> fields) {
Set<Field> requireDefaults = new HashSet<Field>();
DefaultInstances generator = DefaultInstances.DEFAULT;
for (Field field : fields) {
Class<?> type = field.getType();
// First, ignore primitive fields
if (!PrimitiveUtils.isPrimitive(type)) {
// Next, see if we actually can generate a default value
if (generator.getDefault(type) != null) {
// If so, require it
requireDefaults.add(field);
}
}
}
return requireDefaults;
}
// Used to filter out irrelevant fields
private static List<Field> getFields(Class type) {
private static List<Field> getFields(Class type, Class superclassExclude) {
List<Field> result = new ArrayList<Field>();
// Retrieve every private and public field
@@ -208,7 +371,10 @@ public class StructureModifier<TField> {
int mod = field.getModifiers();
// Ignore static, final and "abstract packet" fields
if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod) && !field.getDeclaringClass().equals(Packet.class)) {
if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod) && (
superclassExclude == null || !field.getDeclaringClass().equals(superclassExclude)
)) {
result.add(field);
}
}
@@ -1,8 +1,24 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect;
import java.lang.reflect.Field;
/**
* Represents a field that will revert to its original state when this class is garbaged collected.
*
@@ -0,0 +1,51 @@
package com.comphenix.protocol.reflect.instances;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* Provides simple constructors for collection interfaces.
* @author Kristian
*/
public class CollectionGenerator implements InstanceProvider {
/**
* Shared instance of this generator.
*/
public static CollectionGenerator INSTANCE = new CollectionGenerator();
@Override
public Object create(@Nullable Class<?> type) {
// Standard collection types
if (type.isInterface()) {
if (type.equals(Collection.class) || type.equals(List.class))
return new ArrayList<Object>();
else if (type.equals(Set.class))
return new HashSet<Object>();
else if (type.equals(Map.class))
return new HashMap<Object, Object>();
else if (type.equals(SortedSet.class))
return new TreeSet<Object>();
else if (type.equals(SortedMap.class))
return new TreeMap<Object, Object>();
else if (type.equals(Queue.class))
return new LinkedList<Object>();
}
// Cannot provide an instance
return null;
}
}
@@ -0,0 +1,210 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect.instances;
import java.lang.reflect.Constructor;
import java.util.*;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
/**
* Used to construct default instances of any type.
* @author Kristian
*
*/
public class DefaultInstances {
/**
* Standard default instance provider.
*/
public static DefaultInstances DEFAULT = DefaultInstances.fromArray(
PrimitiveGenerator.INSTANCE, CollectionGenerator.INSTANCE);
/**
* The maximum height of the hierachy of creates types. Used to prevent cycles.
*/
private final static int MAXIMUM_RECURSION = 20;
/**
* Ordered list of instance provider, from highest priority to lowest.
*/
private ImmutableList<InstanceProvider> registered;
/**
* Construct a default instance generator using the given instance providers.
* @param registered - list of instance providers.
*/
public DefaultInstances(ImmutableList<InstanceProvider> registered) {
this.registered = registered;
}
/**
* Construct a default instance generator using the given instance providers.
* @param instaceProviders - array of instance providers.
*/
public DefaultInstances(InstanceProvider... instaceProviders) {
this(ImmutableList.copyOf(instaceProviders));
}
/**
* Construct a default instance generator using the given instance providers.
* @param instaceProviders - array of instance providers.
* @return An default instance generator.
*/
public static DefaultInstances fromArray(InstanceProvider... instaceProviders) {
return new DefaultInstances(ImmutableList.copyOf(instaceProviders));
}
/**
* Retrieves a immutable list of every default object providers that generates instances.
* @return Table of instance providers.
*/
public ImmutableList<InstanceProvider> getRegistered() {
return registered;
}
/**
* Retrieves a default instance or value that is assignable to this type.
* <p>
* This includes, but isn't limited too:
* <ul>
* <li>Primitive types. Returns either zero or null.</li>
* <li>Primitive wrappers.</li>
* <li>String types. Returns an empty string.</li>
* <li>Arrays. Returns a zero-length array of the same type.</li>
* <li>Enums. Returns the first declared element.</li>
* <li>Collection interfaces, such as List and Set. Returns the most appropriate empty container.</li>
* <li>Any type with a public constructor that has parameters with defaults.</li>
* </ul>
* </ul>
* @param type - the type to construct a default value.
* @return A default value/instance, or NULL if not possible.
*/
public <T> T getDefault(Class<T> type) {
return getDefaultInternal(type, registered, 0);
}
/**
* Retrieves a default instance or value that is assignable to this type.
* <p>
* This includes, but isn't limited too:
* <ul>
* <li>Primitive types. Returns either zero or null.</li>
* <li>Primitive wrappers.</li>
* <li>String types. Returns an empty string.</li>
* <li>Arrays. Returns a zero-length array of the same type.</li>
* <li>Enums. Returns the first declared element.</li>
* <li>Collection interfaces, such as List and Set. Returns the most appropriate empty container.</li>
* <li>Any type with a public constructor that has parameters with defaults.</li>
* </ul>
* </ul>
* @param type - the type to construct a default value.
* @param providers - instance providers used during the
* @return A default value/instance, or NULL if not possible.
*/
public <T> T getDefault(Class<T> type, List<InstanceProvider> providers) {
return getDefaultInternal(type, providers, 0);
}
@SuppressWarnings("unchecked")
private <T> T getDefaultInternal(Class<T> type, List<InstanceProvider> providers, int recursionLevel) {
// Guard against recursion
if (recursionLevel > MAXIMUM_RECURSION) {
return null;
}
for (InstanceProvider generator : providers) {
Object value = generator.create(type);
if (value != null)
return (T) value;
}
Constructor<T> minimum = null;
int lastCount = Integer.MAX_VALUE;
// Find the constructor with the fewest parameters
for (Constructor<?> candidate : type.getConstructors()) {
Class<?>[] types = candidate.getParameterTypes();
// Note that we don't allow recursive types - that is, types that
// require itself in the constructor.
if (types.length < lastCount) {
if (!contains(types, type)) {
minimum = (Constructor<T>) candidate;
lastCount = types.length;
// Don't loop again if we've already found the best possible constructor
if (lastCount == 0)
break;
}
}
}
// Create the type with this constructor using default values. This might fail, though.
try {
if (minimum != null) {
Object[] params = new Object[lastCount];
Class<?>[] types = minimum.getParameterTypes();
// Fill out
for (int i = 0; i < lastCount; i++) {
params[i] = getDefaultInternal(types[i], providers, recursionLevel + 1);
}
return createInstance(type, minimum, types, params);
}
} catch (Exception e) {
// Nope, we couldn't create this type
}
// No suitable default value could be found
return null;
}
/**
* Used by the default instance provider to create a class from a given constructor.
* The default method uses reflection.
* @param type - the type to create.
* @param constructor - the constructor to use.
* @param types - type of each parameter in order.
* @param params - value of each parameter in order.
* @return The constructed instance.
*/
protected <T> T createInstance(Class<T> type, Constructor<T> constructor, Class<?>[] types, Object[] params) {
try {
return (T) constructor.newInstance(params);
} catch (Exception e) {
// Cannot create it
return null;
}
}
// We avoid Apache's utility methods to stay backwards compatible
protected <T> boolean contains(T[] elements, T elementToFind) {
// Search for the given element in the array
for (T element : elements) {
if (Objects.equal(elementToFind, element))
return true;
}
return false;
}
}
@@ -0,0 +1,18 @@
package com.comphenix.protocol.reflect.instances;
import javax.annotation.Nullable;
/**
* Represents a type generator for specific types.
*
* @author Kristian
*/
public interface InstanceProvider {
/**
* Create an instance given a type, if possible.
* @param type - type to create.
* @return The instance, or NULL if the type cannot be created.
*/
public abstract Object create(@Nullable Class<?> type);
}
@@ -0,0 +1,62 @@
package com.comphenix.protocol.reflect.instances;
import java.lang.reflect.Array;
import javax.annotation.Nullable;
import com.comphenix.protocol.reflect.PrimitiveUtils;
import com.google.common.base.Defaults;
/**
* Provides constructors for primtive types, wrappers, arrays and strings.
* @author Kristian
*/
public class PrimitiveGenerator implements InstanceProvider {
/**
* Default value for Strings.
*/
public static final String STRING_DEFAULT = "";
/**
* Shared instance of this generator.
*/
public static PrimitiveGenerator INSTANCE = new PrimitiveGenerator(STRING_DEFAULT);
// Our default string value
private String stringDefault;
public PrimitiveGenerator(String stringDefault) {
this.stringDefault = stringDefault;
}
/**
* Retrieve the string default.
* @return Default instance of a string.
*/
public String getStringDefault() {
return stringDefault;
}
@Override
public Object create(@Nullable Class<?> type) {
if (PrimitiveUtils.isPrimitive(type)) {
return Defaults.defaultValue(type);
} else if (PrimitiveUtils.isWrapperType(type)) {
return Defaults.defaultValue(PrimitiveUtils.unwrap(type));
} else if (type.isArray()) {
Class<?> arrayType = type.getComponentType();
return Array.newInstance(arrayType, 0);
} else if (type.isEnum()) {
Object[] values = type.getEnumConstants();
if (values != null && values.length > 0)
return values[0];
} else if (type.equals(String.class)) {
return stringDefault;
}
// Cannot handle this type
return null;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
name: ProtocolLib
version: 1.0.0
version: 1.1.0
description: Provides read/write access to the Minecraft protocol.
author: Comphenix
website: http://www.comphenix.net/ProtocolLib
@@ -0,0 +1,77 @@
package com.comphenix.protocol.injector;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import com.comphenix.protocol.concurrency.SortedCopyOnWriteArray;
import com.comphenix.protocol.events.ListenerPriority;
import com.google.common.primitives.Ints;
public class SortedCopyOnWriteArrayTest {
@Test
public void testInsertion() {
final int MAX_NUMBER = 50;
SortedCopyOnWriteArray<Integer> test = new SortedCopyOnWriteArray<Integer>();
// Generate some numbers
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 0; i < MAX_NUMBER; i++) {
numbers.add(i);
}
// Random insertion to test it all
Collections.shuffle(numbers);
// O(n^2) of course, so don't use too large numbers
for (int i = 0; i < MAX_NUMBER; i++) {
test.add(numbers.get(i));
}
// Check that everything is correct
for (int i = 0; i < MAX_NUMBER; i++) {
assertEquals((Integer) i, test.get(i));
}
}
@Test
public void testOrder() {
PriorityStuff a = new PriorityStuff(ListenerPriority.HIGH, 1);
PriorityStuff b = new PriorityStuff(ListenerPriority.NORMAL, 2);
PriorityStuff c = new PriorityStuff(ListenerPriority.NORMAL, 3);
SortedCopyOnWriteArray<PriorityStuff> test = new SortedCopyOnWriteArray<PriorityStuff>();
test.add(a);
test.add(b);
test.add(c);
// Make sure the normal's are in the right order
assertEquals(2, test.get(0).id);
assertEquals(3, test.get(1).id);
}
private class PriorityStuff implements Comparable<PriorityStuff> {
public ListenerPriority priority;
public int id;
public PriorityStuff(ListenerPriority priority, int id) {
this.priority = priority;
this.id = id;
}
@Override
public int compareTo(PriorityStuff other) {
// This ensures that lower priority listeners are executed first
return Ints.compare(priority.getSlot(),
other.priority.getSlot());
}
}
}
@@ -6,15 +6,17 @@ import net.minecraft.server.Packet103SetSlot;
import org.junit.Test;
import com.avaje.ebeaninternal.server.cluster.Packet;
import com.comphenix.protocol.reflect.StructureModifier;
public class StructureModifierTest {
@Test
public void test() throws IllegalAccessException {
public void test() throws FieldAccessException {
Packet103SetSlot move = new Packet103SetSlot();
StructureModifier<Object> modifier = new StructureModifier<Object>(Packet103SetSlot.class);
StructureModifier<Object> modifier = new StructureModifier<Object>(
Packet103SetSlot.class, Packet.class, true);
move.a = 1;
int value = (Integer) modifier.withTarget(move).withType(int.class).read(0);
+133
View File
@@ -0,0 +1,133 @@
ProtocolLib
===========
Certain tasks are impossible to perform with the standard Bukkit API, and may require
working with and even modify Minecraft directly. A common technique is to modify incoming
and outgoing [packets](http://www.wiki.vg/Protocol), or inject custom packets into the
stream. This is quite cumbersome to do, however, and most implementations will break
as soon as a new version of Minecraft has been released, mostly due to obfuscation.
Critically, different plugins that use this approach may _hook_ into the same classes,
with unpredictable outcomes. More than often this causes plugins to crash, but it may also
lead to more subtle bugs.
### Resources
* [JavaDoc](http://aadnk.github.com/ProtocolLib/Javadoc/)
A new API
---------
__ProtocolLib__ attempts to solve this problem by providing a event API, much like Bukkit,
that allow plugins to monitor, modify or cancel packets sent and received. But more importantly,
the API also hides all the gritty, obfuscated classes with a simple index based read/write system.
You no longer have to reference CraftBukkit!
### Using ProtocolLib
To use the library, first add ProtocolLib.jar to your Java build path. Then, add ProtocolLib
as a dependency (or soft-dependency, if you can live without it) to your plugin.yml file:
depends: [ProtocolLib]
Then get a reference to ProtocolManager in onLoad() and you're good to go.
private ProtocolManager protocolManager;
public void onLoad() {
protocolManager = ProtocolLibrary.getProtocolManager();
}
To listen for packets sent by the server to a client, add a server-side listener:
// Disable all sound effects
protocolManager.addPacketListener(
new PacketAdapter(this, ConnectionSide.SERVER_SIDE, ListenerPriority.NORMAL, 0x3E) {
@Override
public void onPacketSending(PacketEvent event) {
// Item packets
switch (event.getPacketID()) {
case 0x3E: // Sound effect
event.setCancelled(true);
break;
}
}
});
It's also possible to read and modify the content of these packets. For instance, you can create a global
censor by listening for Packet3Chat events:
// Censor
protocolManager.addPacketListener(
new PacketAdapter(this, ConnectionSide.CLIENT_SIDE, ListenerPriority.NORMAL, 0x03) {
@Override
public void onPacketReceiving(PacketEvent event) {
if (event.getPacketID() == 0x03) {
try {
PacketContainer packet = event.getPacket();
String message = packet.getSpecificModifier(String.class).read(0);
if (message.contains("shit") || message.contains("damn")) {
event.setCancelled(true);
event.getPlayer().sendMessage("Bad manners!");
}
} catch (FieldAccessException e) {
getLogger().log(Level.SEVERE, "Couldn't access field.", e);
}
}
}
});
### Sending packets
Normally, you might have to do something ugly like the following:
Packet60Explosion fakeExplosion = new Packet60Explosion();
fakeExplosion.a = player.getLocation().getX();
fakeExplosion.b = player.getLocation().getY();
fakeExplosion.c = player.getLocation().getZ();
fakeExplosion.d = 3.0F;
fakeExplosion.e = new ArrayList<Object>();
((CraftPlayer) player).getHandle().netServerHandler.sendPacket(fakeExplosion);
But with ProtocolLib, you can turn that into something more manageable. Notice that
you don't have to create an ArrayList this version:
PacketContainer fakeExplosion = protocolManager.createPacket(60);
fakeExplosion.getSpecificModifier(double.class).
write(0, player.getLocation().getX()).
write(1, player.getLocation().getY()).
write(2, player.getLocation().getZ());
fakeExplosion.getSpecificModifier(float.class).
write(0, 3.0F);
protocolManager.sendServerPacket(player, fakeExplosion);
Compatiblity
------------
One of the main goals of this project was to achive maximum compatibility with craftBukkit. And the end
result is quite flexible - in tests I successfully ran an unmodified ProtocolLib on CraftBukkit 1.8.0, and
it should be resiliant against future changes. It's likely that I won't have to update ProtocolLib for
anything but bug and performance fixes.
How is this possible? It all comes down to reflection in the end. Essentially, no name is hard coded -
every field, method and class is deduced by looking at field types, package names or parameter
types. It's remarkably consistent across different versions.
### Incompatiblity
The following plugins (to be expanded) are not compatible with ProtocolLib:
* TagAPI