/*
* Copyright (c) 2010 SimpleServer authors (see CONTRIBUTORS)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package simpleserver;
import static simpleserver.lang.Translations.t;
import static simpleserver.util.Util.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.xml.sax.SAXException;
import simpleserver.Coordinate.Dimension;
import simpleserver.bot.BotController.ConnectException;
import simpleserver.bot.Giver;
import simpleserver.bot.Teleporter;
import simpleserver.command.ExternalCommand;
import simpleserver.command.PlayerCommand;
import simpleserver.config.KitList.Kit;
import simpleserver.config.data.Stats.StatField;
import simpleserver.config.xml.Area;
import simpleserver.config.xml.CommandConfig;
import simpleserver.config.xml.CommandConfig.Forwarding;
import simpleserver.config.xml.Event;
import simpleserver.config.xml.Group;
import simpleserver.config.xml.Permission;
import simpleserver.message.AbstractChat;
import simpleserver.message.Chat;
import simpleserver.message.GlobalChat;
import simpleserver.message.Message;
import simpleserver.stream.Encryption;
import simpleserver.stream.Encryption.ClientEncryption;
import simpleserver.stream.Encryption.ServerEncryption;
import simpleserver.stream.StreamTunnel;
public class Player {
private final long connected;
private final Socket extsocket;
private final Server server;
private Socket intsocket;
private StreamTunnel serverToClient;
private StreamTunnel clientToServer;
private Watchdog watchdog;
public ServerEncryption serverEncryption = new Encryption.ServerEncryption();
public ClientEncryption clientEncryption = new Encryption.ClientEncryption();
private String name = null;
private String renameName = null;
private String connectionHash;
private boolean closed = false;
private boolean isKicked = false;
private Action attemptedAction;
private boolean instantDestroy = false;
private boolean godMode = false;
private String kickMsg = null;
public Position position;
private Position deathPlace;
private float health = 0;
private short experience = 0;
private int group = 0;
private int entityId = 0;
private Group groupObject = null;
private boolean isRobot = false;
// player is not authenticated with minecraft.net:
private boolean guest = false;
private boolean usedAuthenticator = false;
private int blocksPlaced = 0;
private int blocksDestroyed = 0;
private Player reply = null;
private String lastCommand = "";
private AbstractChat chatType;
private Queue<String> messages = new ConcurrentLinkedQueue<String>();
private Queue<String> forwardMessages = new ConcurrentLinkedQueue<String>();
private Queue<PlayerVisitRequest> visitreqs = new ConcurrentLinkedQueue<PlayerVisitRequest>();
private Coordinate chestPlaced;
private Coordinate chestOpened;
private String nextChestName;
// temporary coordinate storage for /myarea command
public Coordinate areastart;
public Coordinate areaend;
private long lastTeleport;
private short experienceLevel;
public ConcurrentHashMap<String, String> vars; // temporary player-scope
// Script variables
private long lastEvent;
private HashSet<Area> currentAreas = new HashSet<Area>();
public Player(Socket inc, Server parent) {
connected = System.currentTimeMillis();
position = new Position();
server = parent;
chatType = new GlobalChat(this);
extsocket = inc;
vars = new ConcurrentHashMap<String, String>();
if (server.isRobot(getIPAddress())) {
println("Robot Heartbeat: " + getIPAddress()
+ ".");
isRobot = true;
} else {
println("IP Connection from " + getIPAddress()
+ "!");
}
if (server.isIPBanned(getIPAddress())) {
println("IP " + getIPAddress() + " is banned!");
cleanup();
return;
}
server.requestTracker.addRequest(getIPAddress());
try {
InetAddress localAddress = InetAddress.getByName(Server.addressFactory.getNextAddress());
intsocket = new Socket(InetAddress.getByName(null),
server.options.getInt("internalPort"),
localAddress, 0);
} catch (Exception e) {
try {
intsocket = new Socket(InetAddress.getByName(null), server.options.getInt("internalPort"));
} catch (Exception E) {
e.printStackTrace();
if (server.config.properties.getBoolean("exitOnFailure")) {
server.stop();
} else {
server.restart();
}
cleanup();
return;
}
}
watchdog = new Watchdog();
try {
serverToClient = new StreamTunnel(intsocket.getInputStream(),
extsocket.getOutputStream(), true, this);
clientToServer = new StreamTunnel(extsocket.getInputStream(),
intsocket.getOutputStream(), false,
this);
} catch (IOException e) {
e.printStackTrace();
cleanup();
return;
}
if (isRobot) {
server.addRobotPort(intsocket.getLocalPort());
}
watchdog.start();
}
public boolean setName(String name) {
renameName = server.data.players.getRenameName(name);
name = name.trim();
if (name.length() == 0 || this.name != null) {
kick(t("Invalid Name!"));
return false;
}
if (name == "Player") {
kick(t("Too many guests in server!"));
return false;
}
if (!guest && server.config.properties.getBoolean("useWhitelist")
&& !server.whitelist.isWhitelisted(name)) {
kick(t("You are not whitelisted!"));
return false;
}
if (server.playerList.findPlayerExact(name) != null) {
kick(t("Player already in server!"));
return false;
}
this.name = name;
updateGroup();
watchdog.setName("PlayerWatchdog-" + name);
server.connectionLog("player", extsocket, name);
if (server.numPlayers() == 0) {
server.time.set();
}
server.playerList.addPlayer(this);
return true;
}
public String getName() {
return renameName;
}
public String getName(boolean original) {
return (original) ? name : renameName;
}
public String getRealName() {
return server.data.players.getRealName(name);
}
public void updateRealName(String name) {
server.data.players.setRealName(name);
}
public String getConnectionHash() {
if (connectionHash == null) {
connectionHash = server.nextHash();
}
return connectionHash;
}
public String getLoginHash() throws NoSuchAlgorithmException, UnsupportedEncodingException {
return clientEncryption.getLoginHash(getConnectionHash());
}
public double distanceTo(Player player) {
return Math.sqrt(Math.pow(x() - player.x(), 2) + Math.pow(y() - player.y(), 2) + Math.pow(z() - player.z(), 2));
}
public long getConnectedAt() {
return connected;
}
public boolean isAttemptLock() {
return attemptedAction == Action.Lock;
}
public void setAttemptedAction(Action action) {
attemptedAction = action;
}
public boolean instantDestroyEnabled() {
return instantDestroy;
}
public void toggleInstantDestroy() {
instantDestroy = !instantDestroy;
}
public Server getServer() {
return server;
}
public void setChat(AbstractChat chat) {
chatType = chat;
}
public String getChatRoom() {
return chatType.toString();
}
public void sendMessage(String message) {
sendMessage(chatType, message);
}
public void sendMessage(String message, boolean build) {
sendMessage(chatType, message, build);
}
public void sendMessage(Chat messageType, String message) {
server.getMessager().propagate(messageType, message);
}
public void sendMessage(Chat messageType, String message, boolean build) {
server.getMessager().propagate(messageType, message, build);
}
public void forwardMessage(String message) {
forwardMessages.add(message);
}
public boolean hasForwardMessages() {
return !forwardMessages.isEmpty();
}
public boolean hasMessages() {
return !messages.isEmpty();
}
public void addMessage(Color color, String format, Object... args) {
addMessage(color, String.format(format, args));
}
public void addMessage(Color color, String message) {
addMessage(color + message);
}
public void addMessage(String format, Object... args) {
addMessage(String.format(format, args));
}
public void addCaptionedMessage(String caption, String format, Object... args) {
addMessage("%s%s: %s%s", Color.GRAY, caption, Color.WHITE, String.format(format, args));
}
public void addMessage(String msg) {
messages.add(new Message(msg).buildMessage(true));
}
public void addTMessage(Color color, String format, Object... args) {
addMessage(color + t(format, args));
}
public void addTMessage(Color color, String message) {
addMessage(color + t(message));
}
public void addTMessage(String msg) {
addMessage(t(msg));
}
public void addTCaptionedTMessage(String caption, String format, Object... args) {
addMessage("%s%s: %s%s", Color.GRAY, t(caption), Color.WHITE, t(format, args));
}
public void addTCaptionedMessage(String caption, String format, Object... args) {
addMessage("%s%s: %s%s", Color.GRAY, t(caption),
Color.WHITE, String.format(format, args));
}
public String getForwardMessage() {
return forwardMessages.remove();
}
public String getMessage() {
return messages.remove();
}
public void addVisitRequest(Player source) {
visitreqs.add(new PlayerVisitRequest(source));
}
public void handleVisitRequests() {
while (visitreqs.size() > 0) {
PlayerVisitRequest req = visitreqs.remove();
if (System.currentTimeMillis() < req.timestamp + 10000 && server.findPlayerExact(req.source.getName()) != null) {
req.source.addTMessage(Color.GRAY, "Request accepted!");
req.source.teleportTo(this);
}
}
}
public void kick(String reason) {
kickMsg = reason;
isKicked = true;
serverToClient.stop();
clientToServer.stop();
}
public boolean isKicked() {
return isKicked;
}
public void setKicked(boolean b) {
isKicked = b;
}
public String getKickMsg() {
return kickMsg;
}
public boolean isMuted() {
return server.mutelist.isMuted(name);
}
public boolean isRobot() {
return isRobot;
}
public boolean godModeEnabled() {
return godMode;
}
public void toggleGodMode() {
godMode = !godMode;
}
public int getEntityId() {
return entityId;
}
public void setEntityId(int readInt) {
entityId = readInt;
}
public int getGroupId() {
return group;
}
public Group getGroup() {
return groupObject;
}
public void setGuest(boolean guest) {
this.guest = guest;
}
public boolean isGuest() {
return guest;
}
public void setUsedAuthenticator(boolean usedAuthenticator) {
this.usedAuthenticator = usedAuthenticator;
}
public boolean usedAuthenticator() {
return usedAuthenticator;
}
public String getIPAddress() {
return extsocket.getInetAddress().getHostAddress();
}
public InetAddress getInetAddress() {
return extsocket.getInetAddress();
}
public boolean ignoresChestLocks() {
return groupObject.ignoreChestLocks;
}
private void setDeathPlace(Position deathPosition) {
deathPlace = deathPosition;
}
public Position getDeathPlace() {
return deathPlace;
}
public float getHealth() {
return health;
}
public void updateHealth(float health) {
this.health = health;
if (health <= 0) {
setDeathPlace(new Position(position()));
}
}
public short getExperience() {
return experience;
}
public short getExperienceLevel() {
return experienceLevel;
}
public void updateExperience(float bar, short level, short experience) {
experienceLevel = level;
this.experience = experience;
}
public double x() {
return position.x;
}
public double y() {
return position.y;
}
public double z() {
return position.z;
}
public Coordinate position() {
return position.coordinate();
}
public float yaw() {
return position.yaw;
}
public float pitch() {
return position.pitch;
}
public String parseCommand(String message, boolean overridePermissions) {
// TODO: Handle aliases of external commands
if (closed) {
return null;
}
// Repeat last command
if (message.equals(server.getCommandParser().commandPrefix() + "!")) {
message = lastCommand;
} else {
lastCommand = message;
}
String commandName = message.split(" ")[0].substring(1).toLowerCase();
String args = commandName.length() + 1 >= message.length() ? "" : message.substring(commandName.length() + 2);
CommandConfig config = server.config.commands.getTopConfig(commandName);
String originalName = config == null ? commandName : config.originalName;
PlayerCommand command = server.resolvePlayerCommand(originalName, groupObject);
if (config != null && !overridePermissions) {
Permission permission = server.config.getCommandPermission(config.name, args, position.coordinate());
if (!permission.contains(this)) {
addTMessage(Color.RED, "Insufficient permission.");
return null;
}
}
try {
if (server.options.getBoolean("enableEvents") && config.event != null) {
Event e = server.eventhost.findEvent(config.event);
if (e != null) {
ArrayList<String> arguments = new ArrayList<String>();
if (!args.equals("")) {
arguments = new ArrayList<String>(java.util.Arrays.asList(args.split("\\s+")));
}
server.eventhost.execute(e, this, true, arguments);
} else {
System.out.println("Error in player command " + originalName + ": Event " + config.event + " not found!");
}
}
} catch (NullPointerException e) {
System.out.println("Error evaluating player command: " + originalName);
}
if (!(command instanceof ExternalCommand) && (config == null || config.forwarding != Forwarding.ONLY)) {
command.execute(this, message);
}
if (command instanceof ExternalCommand) {
// commands with bound events have to be forwarded explicitly
// (to prevent unknown command error by server)
if (config.event != null && config.forwarding == Forwarding.NONE) {
return null;
} else {
return "/" + originalName + " " + args;
}
} else if ((config != null && config.forwarding != Forwarding.NONE) || server.config.properties.getBoolean("forwardAllCommands")) {
return message;
} else {
return null;
}
}
public void execute(Class<? extends PlayerCommand> c) {
execute(c, "");
}
public void execute(Class<? extends PlayerCommand> c, String arguments) {
server.getCommandParser().getPlayerCommand(c).execute(this, "a " + arguments);
}
public void teleportTo(Player target) {
server.runCommand("tp", getName() + " " + target.getName());
}
public void sendMOTD() {
String[] lines = server.motd.getMOTD().split("\\r?\\n");
for (String line : lines) {
addMessage(line);
}
}
public void give(int id, int amount) {
String baseCommand = getName() + " " + id + " ";
for (int c = 0; c < amount / 64; ++c) {
server.runCommand("give", baseCommand + 64);
}
if (amount % 64 != 0) {
server.runCommand("give", baseCommand + amount % 64);
}
}
public void give(int id, short damage, int amount) throws ConnectException {
if (damage == 0) {
give(id, amount);
} else {
Giver giver = new Giver(this);
for (int c = 0; c < amount / 64; ++c) {
giver.add(id, 64, damage);
}
if (amount % 64 != 0) {
giver.add(id, amount % 64, damage);
}
server.bots.connect(giver);
}
}
public void give(Kit kit) throws ConnectException {
Giver giver = new Giver(this);
int invSize = 45;
int slot = invSize;
for (Kit.Entry e : kit.items) {
if (e.damage() == 0) {
give(e.item(), e.amount());
} else {
int restAmount = e.amount();
while (restAmount > 0 && --slot >= 0) {
giver.add(e.item(), Math.min(restAmount, 64), e.damage());
restAmount -= 64;
if (slot == 0) {
slot = invSize;
server.bots.connect(giver);
giver = new Giver(this);
}
}
}
}
if (slot != invSize) {
server.bots.connect(giver);
}
}
public void updateGroup() {
try {
groupObject = server.config.getGroup(this);
} catch (SAXException e) {
println("A player could not be assigned to any group. (" + e + ")");
kick("You could not be asigned to any group.");
return;
}
group = groupObject.id;
}
public void placedBlock() {
blocksPlaced += 1;
}
public void destroyedBlock() {
blocksDestroyed += 1;
}
public Integer[] stats() {
Integer[] stats = new Integer[4];
stats[0] = (int) (System.currentTimeMillis() - connected) / 1000 / 60;
stats[1] = server.data.players.stats.get(this, StatField.PLAY_TIME) + stats[0];
stats[2] = server.data.players.stats.add(this, StatField.BLOCKS_PLACED, blocksPlaced);
stats[3] = server.data.players.stats.add(this, StatField.BLOCKS_DESTROYED, blocksDestroyed);
blocksPlaced = 0;
blocksDestroyed = 0;
server.data.save();
return stats;
}
public void setReply(Player answer) {
// set Player to reply with !reply command
reply = answer;
}
public Player getReply() {
return reply;
}
public void close() {
if (serverToClient != null) {
serverToClient.stop();
}
if (clientToServer != null) {
clientToServer.stop();
}
if (name != null) {
server.authenticator.unbanLogin(this);
if (usedAuthenticator) {
if (guest) {
server.authenticator.releaseGuestName(name);
} else {
server.authenticator.rememberAuthentication(name, getIPAddress());
}
} else if (guest) {
if (isKicked) {
server.authenticator.releaseGuestName(name);
} else {
server.authenticator.rememberGuest(name, getIPAddress());
}
}
server.data.players.stats.add(this, StatField.PLAY_TIME, (int) (System.currentTimeMillis() - connected) / 1000 / 60);
server.data.players.stats.add(this, StatField.BLOCKS_DESTROYED, blocksDestroyed);
server.data.players.stats.add(this, StatField.BLOCKS_PLACED, blocksPlaced);
server.data.save();
server.playerList.removePlayer(this);
name = renameName = null;
}
}
private void cleanup() {
if (!closed) {
closed = true;
entityId = 0;
close();
try {
extsocket.close();
} catch (Exception e) {
}
try {
intsocket.close();
} catch (Exception e) {
}
if (!isRobot) {
println("Socket Closed: "
+ extsocket.getInetAddress().getHostAddress());
}
}
}
private class PlayerVisitRequest {
public Player source;
public long timestamp;
public PlayerVisitRequest(Player source) {
timestamp = System.currentTimeMillis();
this.source = source;
}
}
private final class Watchdog extends Thread {
@Override
public void run() {
while (serverToClient.isAlive() || clientToServer.isAlive()) {
if (!serverToClient.isActive() || !clientToServer.isActive()) {
println("Disconnecting " + getIPAddress()
+ " due to inactivity.");
close();
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
cleanup();
}
}
public void placingChest(Coordinate coord) {
chestPlaced = coord;
}
public boolean placedChest(Coordinate coordinate) {
return chestPlaced != null && chestPlaced.equals(coordinate);
}
public void openingChest(Coordinate coordinate) {
chestOpened = coordinate;
}
public Coordinate openedChest() {
return chestOpened;
}
public void setChestName(String name) {
nextChestName = name;
}
public String nextChestName() {
return nextChestName;
}
public enum Action {
Lock, Unlock, Rename;
}
public boolean isAttemptingUnlock() {
return attemptedAction == Action.Unlock;
}
public void setDimension(Dimension dimension) {
position.updateDimension(dimension);
}
public Dimension getDimension() {
return position.dimension();
}
public void teleport(Coordinate coordinate) throws ConnectException, IOException {
teleport(new Position(coordinate));
}
public void teleport(Position position) throws ConnectException, IOException {
if (position.dimension() == getDimension()) {
server.bots.connect(new Teleporter(this, position));
} else {
addTMessage(Color.RED, "You're not in the same dimension as the specified warppoint.");
}
}
public void teleportSelf(Coordinate coordinate) {
teleportSelf(new Position(coordinate));
}
public void teleportSelf(Position position) {
try {
teleport(position);
} catch (Exception e) {
addTMessage(Color.RED, "Teleporting failed.");
return;
}
lastTeleport = System.currentTimeMillis();
}
private int cooldownLeft() {
int cooldown = getGroup().cooldown();
if (lastTeleport > System.currentTimeMillis() - cooldown) {
return (int) (cooldown - System.currentTimeMillis() + lastTeleport);
} else {
return 0;
}
}
public synchronized void teleportWithWarmup(Coordinate coordinate) {
teleportWithWarmup(new Position(coordinate));
}
public synchronized void teleportWithWarmup(Position position) {
int cooldown = cooldownLeft();
if (lastTeleport < 0) {
addTMessage(Color.RED, "You are already waiting for a teleport.");
} else if (cooldown > 0) {
addTMessage(Color.RED, "You have to wait %d seconds before you can teleport again.", cooldown / 1000);
} else {
int warmup = getGroup().warmup();
if (warmup > 0) {
lastTeleport = -1;
Timer timer = new Timer();
timer.schedule(new Warmup(position), warmup);
addTMessage(Color.GRAY, "You will be teleported in %s seconds.", warmup / 1000);
} else {
teleportSelf(position);
}
}
}
public void checkAreaEvents() {
HashSet<Area> areas = new HashSet<Area>(server.config.dimensions.areas(position()));
HashSet<Area> areasCopy = new HashSet<Area>(areas);
HashSet<Area> oldAreas = currentAreas;
areasCopy.removeAll(oldAreas); // -> now contains only newly entered areas
oldAreas.removeAll(areas); // -> now contains only areas not present anymore
for (Area a : areasCopy) { // run area onenter events
if (a.event == null) {
continue;
}
Event e = server.eventhost.findEvent(a.event);
if (e != null) {
ArrayList<String> args = new ArrayList<String>();
args.add("enter");
args.add(a.name);
server.eventhost.execute(e, this, true, args);
} else {
System.out.println("Error in area " + a.name + "/event: Event " + a.event + " not found!");
}
}
for (Area a : oldAreas) { // run area onleave events
if (a.event == null) {
continue;
}
Event e = server.eventhost.findEvent(a.event);
if (e != null) {
ArrayList<String> args = new ArrayList<String>();
args.add("leave");
args.add(a.name);
server.eventhost.execute(e, this, true, args);
} else {
System.out.println("Error in area " + a.name + "/event: Event " + a.event + " not found!");
}
}
currentAreas = areas;
}
public void checkLocationEvents() {
checkAreaEvents();
long currtime = System.currentTimeMillis();
if (currtime < lastEvent + 500) {
return;
}
Iterator<Event> it = server.eventhost.events.keySet().iterator();
while (it.hasNext()) {
Event ev = it.next();
if (!ev.type.equals("plate") || ev.coordinate == null) {
continue;
}
if (position.coordinate().equals(ev.coordinate)) { // matching -> execute
server.eventhost.execute(ev, this, false, null);
lastEvent = currtime;
}
}
}
public void checkButtonEvents(Coordinate c) {
long currtime = System.currentTimeMillis();
if (currtime < lastEvent + 500) {
return;
}
Iterator<Event> it = server.eventhost.events.keySet().iterator();
while (it.hasNext()) {
Event ev = it.next();
if (!ev.type.equals("button") || ev.coordinate == null) {
continue;
}
if ((new Coordinate(c.x(), c.y(), c.z(), position.dimension())).equals(ev.coordinate)) { // matching
// ->
// execute
server.eventhost.execute(ev, this, false, null);
lastEvent = currtime;
}
}
}
private final class Warmup extends TimerTask {
private final Position position;
private Warmup(Position position) {
super();
this.position = position;
}
@Override
public void run() {
teleportSelf(position);
}
}
}