/* * 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.bot; import static simpleserver.util.Util.print; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.concurrent.locks.ReentrantLock; import simpleserver.Coordinate.Dimension; import simpleserver.Main; import simpleserver.Position; import simpleserver.Server; import simpleserver.stream.Encryption.ServerEncryption; public class Bot { protected String name; protected Server server; private boolean connected; private boolean expectDisconnect; protected boolean ready; protected boolean dead; protected int playerEntityId; private Socket socket; protected DataInputStream in; protected DataOutputStream out; ReentrantLock writeLock; protected Position position; protected BotController controller; protected boolean gotFirstPacket = false; private byte lastPacket; private float health; private ServerEncryption encryption = new ServerEncryption(); public Bot(Server server, String name) { this.name = name; this.server = server; position = new Position(); } void connect() throws UnknownHostException, IOException { try { InetAddress localAddress = InetAddress.getByName(Server.addressFactory.getNextAddress()); socket = new Socket(InetAddress.getByName(null), server.options.getInt("internalPort"), localAddress, 0); } catch (Exception e) { socket = new Socket(InetAddress.getByName(null), server.options.getInt("internalPort")); } in = new DataInputStream(socket.getInputStream()); out = new DataOutputStream(socket.getOutputStream()); writeLock = new ReentrantLock(); connected = true; new Tunneler().start(); handshake(); } boolean ninja() { return false; } protected void positionUpdate() throws IOException { } private void keepAlive(int keepAliveId) throws IOException { writeLock.lock(); out.writeByte(0x0); out.writeInt(keepAliveId); writeLock.unlock(); } private void handshake() throws IOException { writeLock.lock(); out.writeByte(2); out.writeByte(Main.protocolVersion); write(name); write("localhost"); out.writeInt(server.options.getInt("internalPort")); out.flush(); writeLock.unlock(); } public void logout() throws IOException { die(); expectDisconnect = true; out.writeByte(0xff); write("quitting"); out.flush(); } protected void login() throws IOException { writeLock.lock(); out.writeByte(0xcd); out.writeByte(0); writeLock.unlock(); } private void sendSharedKey() throws IOException { writeLock.lock(); out.writeByte(0xfc); byte[] key = encryption.getEncryptedSharedKey(); out.writeShort(key.length); out.write(key); byte[] challengeTokenResponse = encryption.encryptChallengeToken(); out.writeShort(challengeTokenResponse.length); out.write(challengeTokenResponse); out.flush(); writeLock.unlock(); } private void respawn() throws IOException { writeLock.lock(); out.writeByte(0xcd); out.writeByte(1); writeLock.unlock(); } protected void ready() throws IOException { ready = true; } protected void walk(double d) { double heading = position.yaw * Math.PI / 180; position.x -= Math.sin(heading) * d; position.z += Math.cos(heading) * d; } protected void ascend(double d) { position.y += d; position.stance += d; if (position.stance - position.y > 1.6 || position.stance - position.y < 0.15) { position.stance = position.y + 0.5; } } protected void sendPosition() throws IOException { writeLock.lock(); position.send(out); writeLock.unlock(); } protected boolean trashdat() { return true; } protected void handlePacket(byte packetId) throws IOException { // System.out.println("Packet: 0x" + Integer.toHexString(packetId)); switch (packetId) { case 0x01: // Login Request int eid = in.readInt(); if (playerEntityId == 0) { playerEntityId = eid; } readUTF16(); in.readByte(); position.dimension = Dimension.get(in.readByte()); in.readByte(); in.readByte(); in.readByte(); break; case 0x0d: // Player Position & Look double x = in.readDouble(); double stance = in.readDouble(); double y = in.readDouble(); double z = in.readDouble(); float yaw = in.readFloat(); float pitch = in.readFloat(); boolean onGround = in.readBoolean(); position.updatePosition(x, y, z, stance); position.updateLook(yaw, pitch); position.updateGround(onGround); if (!ready) { sendPosition(); ready(); } else if (dead) { sendPosition(); dead = false; } positionUpdate(); break; case 0x0b: // Player Position double x2 = in.readDouble(); double stance2 = in.readDouble(); double y2 = in.readDouble(); double z2 = in.readDouble(); boolean onGround2 = in.readBoolean(); position.updatePosition(x2, y2, z2, stance2); position.updateGround(onGround2); positionUpdate(); break; case (byte) 0xff: // Disconnect/Kick String reason = readUTF16(); error(reason); break; case 0x00: // Keep Alive keepAlive(in.readInt()); break; case 0x03: // Chat Message readUTF16(); break; case 0x04: // Time Update in.readLong(); in.readLong(); break; case 0x05: // Entity Equipment in.readInt(); in.readShort(); readItem(); break; case 0x06: // Spawn Position readNBytes(12); break; case 0x07: // Use Entity in.readInt(); in.readInt(); in.readBoolean(); break; case 0x08: // Update Health health = in.readFloat(); in.readShort(); in.readFloat(); if (health <= 0) { dead = true; respawn(); } break; case 0x09: // Respawn position.dimension = Dimension.get((byte) in.readInt()); in.readByte(); in.readByte(); in.readShort(); readUTF16(); break; case 0x0a: // Player in.readBoolean(); break; case 0x0c: // Player Look readNBytes(9); break; case 0x0e: // Player Digging in.readByte(); in.readInt(); in.readByte(); in.readInt(); in.readByte(); break; case 0x0f: // Player Block Placement in.readInt(); in.readByte(); in.readInt(); in.readByte(); readItem(); in.readByte(); in.readByte(); in.readByte(); break; case 0x10: // Holding Change in.readShort(); break; case 0x11: // Use Bed readNBytes(14); break; case 0x12: // Animation readNBytes(5); break; case 0x13: // Entity Action in.readInt(); in.readByte(); in.readInt(); break; case 0x14: // Named Entity Spawn in.readInt(); readUTF16(); readNBytes(16); readUnknownBlob(); break; case 0x16: // Collect Item readNBytes(8); break; case 0x17: // Add Object/Vehicle in.readInt(); in.readByte(); in.readInt(); in.readInt(); in.readInt(); in.readByte(); in.readByte(); int flag = in.readInt(); if (flag > 0) { in.readShort(); in.readShort(); in.readShort(); } break; case 0x18: // Mob Spawn in.readInt(); in.readByte(); in.readInt(); in.readInt(); in.readInt(); in.readByte(); in.readByte(); in.readByte(); in.readShort(); in.readShort(); in.readShort(); readUnknownBlob(); break; case 0x19: // Entity: Painting in.readInt(); readUTF16(); in.readInt(); in.readInt(); in.readInt(); in.readInt(); break; case 0x1a: // Experience Orb in.readInt(); in.readInt(); in.readInt(); in.readInt(); in.readShort(); break; case 0x1b: // Steer Vehicle in.readFloat(); in.readFloat(); in.readBoolean(); in.readBoolean(); break; case 0x1c: // Entity Velocity readNBytes(10); break; case 0x1d: // Destroy Entity byte destroyCount = in.readByte(); if (destroyCount > 0) { readNBytes(destroyCount * 4); } break; case 0x1e: // Entity readNBytes(4); break; case 0x1f: // Entity Relative Move readNBytes(7); break; case 0x20: // Entity Look readNBytes(6); break; case 0x21: // Entity Look and Relative Move readNBytes(9); break; case 0x22: // Entity Teleport readNBytes(18); break; case 0x23: // ???, added in 12w03a in.readInt(); in.readByte(); break; case 0x26: // Entity Status readNBytes(5); break; case 0x27: // Attach Entity in.readInt(); in.readInt(); in.readBoolean(); break; case 0x28: // Entity Metadata in.readInt(); readUnknownBlob(); break; case 0x29: // Entity Effect in.readInt(); in.readByte(); in.readByte(); in.readShort(); break; case 0x2a: // Remove Entity Effect in.readInt(); in.readByte(); break; case 0x2b: // Experience in.readFloat(); in.readShort(); in.readShort(); break; case 0x2c: // Entity Properties in.readInt(); int properties_count = in.readInt(); short list_length = 0; // loop for every property key/value pair for (int i = 0; i < properties_count; i++) { readUTF16(); in.readDouble(); list_length = in.readShort(); // loop for list_length if (list_length > 0) { for (int k = 0; k < list_length; k++) { in.readLong(); in.readLong(); in.readDouble(); in.readByte(); } } } break; case 0x33: // Map Chunk readNBytes(13); readNBytes(in.readInt()); break; case 0x34: // Multi Block Change in.readInt(); in.readInt(); in.readShort(); readNBytes(in.readInt()); break; case 0x35: // Block Change in.readInt(); in.readByte(); in.readInt(); in.readShort(); in.readByte(); break; case 0x36: // Block Action readNBytes(13); break; case 0x37: // Mining progress in.readInt(); in.readInt(); in.readInt(); in.readInt(); in.readByte(); break; case 0x38: // Chunk Bulk readNBytes(in.readShort() * 12 + in.readInt()); short chunkCount = in.readShort(); int dataLength = in.readInt(); in.readBoolean(); readNBytes(chunkCount * 12 + dataLength); break; case 0x3c: // Explosion readNBytes(28); int recordCount = in.readInt(); readNBytes(recordCount * 3); readNBytes(12); break; case 0x3d: // Sound/Particle Effect in.readInt(); in.readInt(); in.readByte(); in.readInt(); in.readInt(); in.readBoolean(); break; case 0x3e: // Named Sound Effect readUTF16(); in.readInt(); in.readInt(); in.readInt(); in.readFloat(); in.readByte(); break; case 0x46: // New/Invalid State readNBytes(2); break; case 0x47: // Thunderbolt readNBytes(17); break; case 0x64: // Open Window in.readByte(); byte invtype = in.readByte(); readUTF16(); in.readByte(); in.readBoolean(); if (invtype == 11) { in.readInt(); } break; case 0x65: // Close Window in.readByte(); break; case 0x66: // Window Click in.readByte(); in.readShort(); in.readByte(); in.readShort(); in.readByte(); readItem(); break; case 0x67: // Set Slot in.readByte(); in.readShort(); readItem(); break; case 0x68: // Window Items in.readByte(); short count = in.readShort(); for (int c = 0; c < count; ++c) { readItem(); } break; case 0x69: // Update Window Property in.readByte(); in.readShort(); in.readShort(); break; case 0x6a: // Transaction in.readByte(); in.readShort(); in.readBoolean(); break; case 0x6b: // Creative Inventory Action in.readShort(); readItem(); break; case (byte) 0x6c: // Enchant Item readNBytes(2); break; case (byte) 0x82: // Update Sign in.readInt(); in.readShort(); in.readInt(); readUTF16(); readUTF16(); readUTF16(); readUTF16(); break; case (byte) 0x83: // Item Data in.readShort(); in.readShort(); short length = in.readShort(); readNBytes(length); break; case (byte) 0x84: // added in 12w06a in.readInt(); in.readShort(); in.readInt(); in.readByte(); short nbtLenght = in.readShort(); if (nbtLenght > 0) { readNBytes(nbtLenght); } break; case (byte) 0x85: // Sign Placement in.readByte(); in.readInt(); in.readInt(); in.readInt(); break; case (byte) 0xc8: // Increment Statistic in.readInt(); in.readInt(); break; case (byte) 0xc9: // Player List Item readUTF16(); in.readBoolean(); in.readShort(); break; case (byte) 0xca: // Player Abilities in.readByte(); in.readFloat(); in.readFloat(); break; case (byte) 0xcb: // Tab-Completion readUTF16(); break; case (byte) 0xcc: // Locale and View Distance readUTF16(); in.readByte(); in.readByte(); in.readByte(); in.readBoolean(); break; case (byte) 0xcd: // Login & Respawn in.readByte(); break; case (byte) 0xce: // create scoreboard readUTF16(); readUTF16(); in.readByte(); break; case (byte) 0xcf: // update score readUTF16(); byte updateRemove = in.readByte(); if (updateRemove == 0) { readUTF16(); in.readInt(); } break; case (byte) 0xd0: // display scoreboard in.readByte(); readUTF16(); break; case (byte) 0xd1: // teams readUTF16(); byte mode = in.readByte(); short playerCount = -1; if (mode == 2 || mode == 0) { readUTF16(); // team display name readUTF16(); // team prefix readUTF16(); // team suffix in.readByte(); // friendly fire } // only ran if 0,3,4 if (mode == 0 || mode == 3 || mode == 4) { playerCount = in.readShort(); if (playerCount != -1) { for (int i = 0; i < playerCount; i++) { readUTF16(); } } } break; case (byte) 0xe6: // ModLoaderMP by SDK in.readInt(); // mod in.readInt(); // packet id readNBytes(in.readInt() * 4); // ints readNBytes(in.readInt() * 4); // floats int sizeString = in.readInt(); // strings for (int i = 0; i < sizeString; i++) { readNBytes(in.readInt()); } break; case (byte) 0xfa: // Plugin Message readUTF16(); short arrayLength = in.readShort(); readNBytes(0xff & arrayLength); break; case (byte) 0xfc: // Encryption Key Response byte[] sharedKey = new byte[in.readShort()]; in.readFully(sharedKey); byte[] challengeTokenResponse = new byte[in.readShort()]; in.readFully(challengeTokenResponse); in = new DataInputStream(new BufferedInputStream(encryption.encryptedInputStream(socket.getInputStream()))); out = new DataOutputStream(new BufferedOutputStream(encryption.encryptedOutputStream(socket.getOutputStream()))); login(); break; case (byte) 0xfd: // Encryption Key Request (server -> client) readUTF16(); byte[] keyBytes = new byte[in.readShort()]; in.readFully(keyBytes); byte[] challengeToken = new byte[in.readShort()]; in.readFully(challengeToken); encryption.setPublicKey(keyBytes); encryption.setChallengeToken(challengeToken); sendSharedKey(); break; case (byte) 0xfe: // Server List Ping break; default: error("Unable to handle packet 0x" + Integer.toHexString(packetId) + " after 0x" + Integer.toHexString(lastPacket)); } lastPacket = packetId; } private void readItem() throws IOException { if (in.readShort() > 0) { in.readByte(); in.readShort(); short length; if ((length = in.readShort()) > 0) { readNBytes(length); } } } private void readUnknownBlob() throws IOException { byte unknown = in.readByte(); while (unknown != 0x7f) { int type = (unknown & 0xE0) >> 5; switch (type) { case 0: in.readByte(); break; case 1: in.readShort(); break; case 2: in.readInt(); break; case 3: in.readFloat(); break; case 4: readUTF16(); break; case 5: readItem(); break; case 6: in.readInt(); in.readInt(); in.readInt(); } unknown = in.readByte(); } } protected String write(String s) throws IOException { byte[] bytes = s.getBytes("UTF-16"); if (s.length() == 0) { out.write((byte) 0x00); out.write((byte) 0x00); return s; } bytes[0] = (byte) ((s.length() >> 8) & 0xFF); bytes[1] = (byte) ((s.length() & 0xFF)); for (byte b : bytes) { out.write(b); } return s; } protected String readUTF16() throws IOException { short length = in.readShort(); byte[] bytes = new byte[length * 2 + 2]; for (short i = 0; i < length * 2; i++) { bytes[i + 2] = in.readByte(); } bytes[0] = (byte) 0xfffffffe; bytes[1] = (byte) 0xffffffff; return new String(bytes, "UTF-16"); } private void readNBytes(int bytes) throws IOException { for (int c = 0; c < bytes; ++c) { in.readByte(); } } protected void die() { connected = false; if (controller != null) { controller.remove(this); } if (trashdat()) { File dat = server.getPlayerFile(name); if (controller != null) { controller.trash(dat); } else { dat.delete(); } } } protected void error(String reason) { die(); if (!expectDisconnect) { print("Bot " + name + " died (" + reason + ")\n"); } } public void setController(BotController controller) { this.controller = controller; } private final class Tunneler extends Thread { @Override public void run() { while (connected) { try { handlePacket(in.readByte()); out.flush(); if (!gotFirstPacket) { gotFirstPacket = true; } } catch (IOException e) { if (!gotFirstPacket) { try { connect(); } catch (Exception e2) { error("Socket closed on reconnect"); } break; } else { error("Socket closed"); } } } } } }