public abstract class GameEngine {

	protected final Client client;
	protected final LocalMap map;
	protected final Camera camera;
	protected final Map<Hash, Player> players;
	protected Player self;

	public GameEngine(Client client) {
		this.client = client;

		map = LocalMap.load(client.getGraphics());
		camera = new Camera(client.getKeyboard());

		players = new HashMap<Hash, Player>();

		self = null;
	}

	public Camera getCamera() {
		return camera;
	}

	public LocalMap getMap() {
		return map;
	}

	public Collection<Player> getPlayers() {
		return players.values();
	}

	public Player getPlayer(Hash id) {
		return players.get(id);
	}

	public void addPlayer(Player player) {
		players.put(player.getID(), player);
	}

	public Player removePlayer(Hash id) {
		return players.remove(id);
	}

	public void init(Player self) {
		this.self = self;
		// Add ourself to the player list
		this.addPlayer(self);

		// Load the appropriate map sectors
		map.setLocation(self.getLocation());

		// Reset the camera rotation
		camera.setRotation(Camera.ROTATION_DEFAULT);
	}

	public void update(long now) {
		// Update the camera
		camera.update(now);
	}

	public final void display(Graphics3D g) {
		// Draw our terrain
		map.draw();

		// Draw all players
		for (Player player : players.values()) {
			Point location = player.getLocation();
			g.drawModel(location.x, location.y, player.getModel());
		}

		// TODO: Draw the click mark
	}

	public final void mouseClicked(int x, int y, boolean left) {
		Graphics3D g = client.getGraphics().get3D();

		g.beginPicking();
		{
			this.display(g);
		}
		Pickable item = g.endPicking(x, y);
		if (item == null) // Nothing was clicked on
			return;

		// The ground was clicked
		if (map.isTileBuffer(item)) {
			// Calculate where was clicked
			Point target = g.translateCoordinates(x, y);
			if (target == null)
				return;

			// Calculate a path from here to where was clicked
			Path path = map.generatePath(self.getLocation(), target);
			if (path == null)
				return;

			// Left click means we want to walk there
			if (left) {
				// TODO: Draw a mark where we clicked

				// Send walk command
				client.getWorldManager().sendWalkTo(path);
			}
		}
		// The model was clicked
		else if (item instanceof Model) {
			// If we clicked ourself, ignore it
			if (item != self.getModel())
				this.handleModelClicked(item, x, y, left);
		}
	}

	private void handleModelClicked(Pickable item, int x, int y, boolean left) {
		for (Player player : players.values()) {
			if (item == player.getModel()) {
				// Left click means action - attack/walk to
				if (left) {
					// TODO: Check if we mean attack or walk to - send command
					System.out.println("Attack/Walk-to: " + player);

					// TODO: Draw a mark where we clicked
				}
				else {
					// TODO: Menu with attack/trade/follow/etc. options
					System.out.println("Show menu for: " + player);
				}

				return;
			}
		}

		// An unknown model was clicked, ignore it
	}

	public Point getLocation() {
		return self.getLocation();
	}

	public void setLocation(Point location) {
		self.setLocation(location);
		map.setLocation(location);
	}
} public class Player extends Entity {

	protected final Hash id;
	protected final String username;
	protected final Point location;
	protected final Model model;

	public Player(Hash id, String name, Point location, Model model) {
		this.id = id;
		this.username = name;
		this.location = location;
		this.model = model;
	}

	@Override
	public Hash getID() {
		return id;
	}

	public String getUsername() {
		return username;
	}

	@Override
	public Point getLocation() {
		return location;
	}

	public Model getModel() {
		return model;
	}

	@Override
	public void setLocation(Point location) {
		if (location.equals(this.location))
			return;

		// moving right: 45�, 90�, or 135�
		if (location.x > this.location.x) {
			// moving up: 45�
			if (location.y > this.location.y)
				model.setRotation(45);
			// moving down: 135�
			else if (location.y < this.location.y)
				model.setRotation(135);
			// else: 90�
			else
				model.setRotation(90);
		}
		// moving left: 225�, 270�, or 315�
		else if (location.x < this.location.x){
			// moving up: 315�
			if (location.y > this.location.y)
				model.setRotation(315);
			// moving down: 225�
			else if (location.y < this.location.y)
				model.setRotation(225);
			// else: 270�
			else
				model.setRotation(270);
		}
		// else: 0�, or 180�
		else {
			// moving up: 0�
			if (location.y > this.location.y)
				model.setRotation(0);
			// else: 180�
			else
				model.setRotation(180);
		}

		this.location.set(location);
	}

	@Override
	public int hashCode() {
		return id.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Player))
			return false;

		Player p = (Player) o;
		return id.equals(p.id);
	}

	@Override
	public String toString() {
		return "player[id = " + id + ", username = '" + username + "', location = " + location + "]";
	}
} public abstract class ClientFrame extends DisplayWindow implements Runnable, MouseListener, KeyListener {
	public static final int LOOP_DELAY = 30;
	public static final int UPDATE_RATE_MAX = 1;
	public static final int UPDATE_RATE_MIN = 10;

	protected boolean running;
	protected int currentFPS;
	protected int updateRate;

	public ClientFrame(String title, int width, int height, BufferedImage[] iconImages) {
		super (title, width, height, iconImages);

		running = false;
		currentFPS = 0;
		this.setUpdateRate(1);
	}

	public Graphics getGraphics() {
		return graphics;
	}

	public int getCurrentFPS() {
		return currentFPS;
	}

	@Override
	public void run() {
		long lastUpdate = 0;
		long lastFrame = 0;
		int frames = 0;
		running = true;

		while (running) {
			long start = System.currentTimeMillis();
			if (start - lastUpdate >= updateRate) {
				lastUpdate = start;
				this.update(start);
			}

			frames++;
			if (start - lastFrame >= 1000) {
				lastFrame = start;
				currentFPS = frames;
				frames = 0;
			}

			// Handle any keyboard input
			keyboard.update(this);

			// Handle any mouse input
			mouse.update(this);

			if (super.isCloseRequested()) {
				running = false;
				break;
			}

			if (super.isUpdateRequired()) {
				graphics.clear();
				this.display(graphics);
				graphics.flush();
			}

			super.update();
		}

		super.destroy();
		this.close();
	}

	public void setUpdateRate(int updateRate) {
		if (updateRate < UPDATE_RATE_MAX)
			updateRate = UPDATE_RATE_MAX;
		else if (updateRate > UPDATE_RATE_MIN)
			updateRate = UPDATE_RATE_MIN;

		this.updateRate = updateRate * 30;
	}

	protected abstract void update(long now);
	public abstract void display(Graphics g);
	protected abstract void close();

	@Override
	public abstract void keyPressed(int keyCode, char keyChar);

	@Override
	public abstract void mouseClicked(int x, int y, boolean left);
} public class Packet {
	private static final Logger log = LoggerFactory.getLogger(Packet.class);

	public static final int HEADER_SIZE = 4 + 4; // type + size

	protected static final CharsetDecoder stringDecoder;

	static {
		stringDecoder = Charset.forName("UTF-8").newDecoder();
	}

	// X_SEND are packets from the client -> server
	// X_RESPONSE are from the server -> client
	public enum Type {
		LOGIN_SEND,				LOGIN_RESPONSE,
		PING_SEND,
		CHAT_SEND,				CHAT_RESPONSE,
								MESSAGE_RESPONSE,
		USE_ITEM_SEND,
								FRIEND_LOGIN_RESPONSE,
		FRIEND_ADD_SEND,		FRIEND_ADD_RESPONSE,
		FRIEND_MESSAGE_SEND,	FRIEND_MESSAGE_RESPONSE,
		FRIEND_REMOVE_SEND,		FRIEND_REMOVE_RESPONSE,
								PLAYERS_ADD_RESPONSE,
								PLAYERS_REMOVE_RESPONSE,
								PLAYERS_UPDATE_RESPONSE,
								INVENTORY_ADD_RESPONSE,
								INVENTORY_REMOVE_RESPONSE,
								INVENTORY_UPDATE_RESPONSE,
		WALK_TO_SEND,
		STAT_UPDATE_SEND,
	}

	protected final Type type;
	protected IoBuffer payload;
	protected final IoSession session;

	public Packet(Type type, IoBuffer payload, IoSession session) {
		this.type = type;
		this.payload = payload;
		this.session = session;
	}

	public IoSession getSession() {
		return session;
	}

	public Type getType() {
		return type;
	}

	protected byte[] getBytes() {
		byte[] bytes = new byte[this.size()];

		payload.rewind();
		payload.get(bytes, 0, bytes.length);

		return bytes;
	}

	protected void setBytes(byte[] bytes) {
		payload = IoBuffer.wrap(bytes).asReadOnlyBuffer();
	}

	public void decrypt(PrivateKey key) {
		try {
			Cipher cipher = Cipher.getInstance("RSA");
			cipher.init(Cipher.DECRYPT_MODE, key);

			byte[] encrypted = this.getBytes();
			byte[] decrypted = cipher.doFinal(encrypted);

			this.setBytes(decrypted);
		}
		catch (Exception e) {
			log.error("Error decrpyting packet: " + e.getMessage());
		}
	}

	public <E extends Enum<E>> E getEnum(Class<E> enumClass) {
		return payload.getEnum(enumClass);
	}

	public String getString() {
		try {
			return payload.getString(stringDecoder);
		}
		catch (CharacterCodingException e) {
			return null;
		}
	}

	public Hash getHash() {
		byte[] data = new byte[Hash.LENGTH];
		payload.get(data, 0, Hash.LENGTH);

		return Hash.fromBytes(data);
	}

	public Point getPoint() {
		int x = this.getShort();
		int y = this.getShort();

		return new Point(x, y);
	}

	public Stat getStat() {
		Stat.Type type = this.getEnum(Stat.Type.class);
		long exp = this.getLong();
		int current = this.getShort();

		return new Stat(type, exp, current);
	}

	public Item getItem() {
		int id = this.getShort();
		long amount = this.getLong();
		boolean equiped = this.getBoolean();

		return new Item(id, amount, equiped);
	}

	public Friend getFriend() {
		Hash id = this.getHash();
		String username = this.getString();
		boolean online = this.getBoolean();

		return new Friend(id, username, online);
	}

	public Date getDate() {
		long time = this.getLong();

		return new Date(time);
	}

	public Path getPath() {
		Path path = new Path();

		int stepCount = this.getShort();
		for (int i = 0;i < stepCount;i++)
			path.append(this.getPoint());

		return path;
	}

	public boolean getBoolean() {
		return payload.get() == 1;
	}

	public byte getByte() {
		return payload.get();
	}

	public short getShort() {
		return payload.getShort();
	}

	public int getInt() {
		return payload.getInt();
	}

	public long getLong() {
		return payload.getLong();
	}

	public void rewind() {
		payload.rewind();
	}

	public int size() {
		return payload.limit();
	}

	@Override
	public String toString() {
		return "packet[" + type + "].length = " + this.size();
	}
} public class PacketBuilder extends Packet {
	private static final Logger log = LoggerFactory.getLogger(PacketBuilder.class);

	public static final int DEFAULT_CAPACITY = 100;

	protected static final CharsetEncoder stringEncoder;

	static {
		stringEncoder = Charset.forName("UTF-8").newEncoder();
	}

	public PacketBuilder(Type type) {
		super (type, IoBuffer.allocate(DEFAULT_CAPACITY).setAutoExpand(true), null);
	}

	public void putEnum(Enum<?> e) {
		payload.putEnum(e);
	}

	public void putString(String s) {
		try {
			payload.putString(s + '\0', stringEncoder);
		}
		catch (CharacterCodingException e) {
			log.error("Error encoding string: " + e);
			payload.put((byte) 0); // Put a null byte to terminate the non-existant string
		}
	}

	public void putHash(Hash hash) {
		payload.put(hash.getBytes(), 0, Hash.LENGTH);
	}

	public void putPoint(Point p) {
		this.putShort((short) p.x);
		this.putShort((short) p.y);
	}

	public void putStat(Stat s) {
		this.putEnum(s.getType());
		this.putLong(s.getExp());
		this.putShort((short) s.getCurrent());
	}

	public void putItem(Item i) {
		this.putShort((short) i.getID());
		this.putLong(i.getAmount());
		this.putBoolean(i.isEquiped());
	}

	public void putFriend(Friend f) {
		this.putHash(f.getID());
		this.putString(f.getUsername());
		this.putBoolean(f.isOnline());
	}

	public void putDate(Date d) {
		this.putLong(d.getTime());
	}

	public void putPath(Path path) {
		this.putShort((short) path.length());
		for (Point step : path)
			this.putPoint(step);
	}

	public void putBoolean(boolean b) {
		payload.put((byte) (b ? 1 : 0));
	}

	public void putByte(byte b) {
		payload.put(b);
	}

	public void putShort(short s) {
		payload.putShort(s);
	}

	public void putInt(int i) {
		payload.putInt(i);
	}

	public void putLong(long l) {
		payload.putLong(l);
	}

	@Override
	public int size() {
		return payload.position();
	}

	public void encrypt(PublicKey key) {
		try {
			Cipher cipher = Cipher.getInstance("RSA");
			cipher.init(Cipher.ENCRYPT_MODE, key);

			byte[] decrypted = super.getBytes();
			byte[] encrypted = cipher.doFinal(decrypted);

			super.setBytes(encrypted);
			payload.position(encrypted.length);
		}
		catch (Exception e) {
			log.error("Error encrypting packet: " + e.getMessage());
		}
	}

	public void clear() {
		payload.clear();
	}

	public IoBuffer getPayload() {
		return payload.getSlice(0, this.size()).asReadOnlyBuffer().rewind();
	}
} public class Friend implements Serializable {
	private static final long serialVersionUID = 1L;

	protected Hash id;
	protected String username;
	protected transient boolean online;

	public Friend(Hash id, String username, boolean online) {
		this.id = id;
		this.username = username;
		this.online = online;
	}

	@SuppressWarnings("unused")
	private Friend() { } // for hibernate

	public Hash getID() {
		return id;
	}

	public String getUsername() {
		return username;
	}

	public boolean isOnline() {
		return online;
	}

	public void setOnline(boolean online) {
		this.online = online;
	}

	@Override
	public int hashCode() {
		return id.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Friend))
			return false;

		Friend f = (Friend) o;
		return id.equals(f.id);
	}

	@Override
	public String toString() {
		return "friend[id = " + id + ", username = '" + username + "', online = " + online + "]";
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		this.setOnline(false); // Default to offline unless we're told otherwise
	}
} public class Hash implements Comparable<Hash>, Serializable {
	private static final Logger log = LoggerFactory.getLogger(Hash.class);
	private static final long serialVersionUID = 1L;

	public static final int LENGTH = 20; // in bytes
	public static final String ALGORITHM = "SHA-1";

	protected static MessageDigest digest;

	static {
		try {
			digest = MessageDigest.getInstance(ALGORITHM);
		}
		catch (NoSuchAlgorithmException e) {
			log.error("No such hashing algorithm: " + ALGORITHM);
			System.exit(1); // fatal error
		}
	}

	public static Hash fromString(String hex) {
		Hash hash = new Hash();
		hash.setHex(hex);

		return hash;
	}

	public static Hash fromBytes(byte[] bytes) {
		Hash hash = new Hash();
		hash.setBytes(bytes);

		return hash;
	}

	protected String hex;

	public Hash(String str) {
		try {
			digest.update(str.getBytes("UTF-8"));
		}
		catch (UnsupportedEncodingException e) { }

		this.setBytes(digest.digest());
	}

	private Hash() {
		hex = null;
	}

	private void setHex(String hex) {
		this.hex = hex;
	}

	private void setBytes(byte[] bytes) {
		Formatter foramtter = new Formatter();

		for (byte b : bytes)
			foramtter.format("%02x", b);

		this.hex = foramtter.toString();
	}

	public byte[] getBytes() {
		byte[] bytes = new byte[LENGTH];

		for (int i = 0;i < LENGTH;i++)
			bytes[i] = (byte) Short.parseShort(hex.substring(2 * i, 2 * (i + 1)), 16);

		return bytes;
	}

	@Override
	public String toString() {
		return hex;
	}

	@Override
	public int hashCode() {
		return hex.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Hash))
			return false;

		Hash h = (Hash) o;
		return hex.equals(h.hex);
	}

	@Override
	public int compareTo(Hash h) {
		return hex.compareTo(h.hex);
	}
} public class Inventory implements Iterable<Item>, Serializable {
	private static final long serialVersionUID = 1L;

	public static final int MAX_SIZE = 40;

	protected List<Item> items;

	public Inventory() {
		items = new ArrayList<Item>();
	}

	public int add(Item item) {
		// TODO: Handle stackable items properly
		synchronized (items) {
			if (items.size() >= MAX_SIZE)
				return -1;

			items.add(item);
			return items.size() - 1;
		}
	}

	public Item get(int index) {
		if (index >= items.size())
			return null;

		return items.get(index);
	}

	public Item remove(int index) {
		if (index >= items.size())
			return null;

		return items.remove(index);
	}

	public int size() {
		return items.size();
	}

	@Override
	public String toString() {
		return "inventory[size = " + items.size() + "]";
	}

	@Override
	public Iterator<Item> iterator() {
		return items.iterator();
	}
} public class Path implements Iterable<Point> {

	protected final Deque<Point> steps;

	public Path() {
		steps = new LinkedList<Point>();
	}

	public Path(Deque<Point> steps) {
		this.steps = steps;
	}

	public void prepend(Point step) {
		steps.addFirst(step);
	}

	public void append(Point step) {
		steps.addLast(step);
	}

	public boolean hasNext() {
		return !steps.isEmpty();
	}

	public Point getNext() {
		return steps.peekFirst();
	}

	public Point removeNext() {
		return steps.pollFirst();
	}

	public Point getLast() {
		return steps.peekLast();
	}

	public int length() {
		return steps.size();
	}

	@Override
	public String toString() {
		return "path[length = " + steps.size() + ", next = " + this.getNext() + ", target = " + this.getLast() + "]";
	}

	@Override
	public Iterator<Point> iterator() {
		return steps.iterator();
	}
} public class PlayerProfile implements Serializable {
	private static final long serialVersionUID = 1L;

	public Hash id;

	@SuppressWarnings("unused")
	private Hash password; // for hibernate

	public String username;
	public Point location;

	public Inventory inventory;
	public StatList stats;
	public FriendList friends;

	public Date registered;
	public Date lastSession;

	@Override
	public int hashCode() {
		return id.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof PlayerProfile))
			return false;

		PlayerProfile p = (PlayerProfile) o;
		return id.equals(p.id);
	}

	@Override
	public String toString() {
		return "profile[id = " + id + ", username = '" + username + "', location = " + location + "]";
	}
} public class FriendList implements Iterable<Friend>, Serializable {
	private static final long serialVersionUID = 1L;

	protected Map<Hash, Friend> friends;

	public FriendList() {
		friends = new HashMap<Hash, Friend>();
	}

	public void add(Friend friend) {
		friends.put(friend.getID(), friend);
	}

	public boolean contains(Hash id) {
		return friends.containsKey(id);
	}

	public Friend get(Hash id) {
		return friends.get(id);
	}

	public Friend remove(Hash id) {
		return friends.remove(id);
	}

	public int size() {
		return friends.size();
	}

	@Override
	public Iterator<Friend> iterator() {
		return friends.values().iterator();
	}
} public class StatList implements Iterable<Stat>, Observer, Serializable {
	private static final long serialVersionUID = 1L;

	public static final int NUM_STATS;

	static {
		NUM_STATS = Stat.Type.values().length;
	}

	protected List<Stat> stats;
	protected transient int combatLevel;
	protected transient int skillTotal;

	public StatList(List<Stat> stats) {
		// Add us as an observer for all the stats
		for (Stat stat : stats)
			stat.addObserver(this);

		this.stats = stats;

		// Calculate the combat level and skill total
		this.update(null, true);
	}

	@SuppressWarnings("unused")
	private StatList() { } // for hibernate

	public Stat get(Stat.Type type) {
		return stats.get(type.ordinal());
	}

	public synchronized int getCombatLevel() {
		return combatLevel;
	}

	public synchronized int getSkillTotal() {
		return skillTotal;
	}

	public int size() {
		return stats.size();
	}

	@Override
	public Iterator<Stat> iterator() {
		return stats.iterator();
	}

	protected synchronized int calcCombatLevel() {
		int total = 0;

		total += this.get(Stat.Type.ATTACK).getLevel();
		total += this.get(Stat.Type.DEFENSE).getLevel();
		total += this.get(Stat.Type.STRENGTH).getLevel();
		total += this.get(Stat.Type.MAGIC).getLevel();
		total += this.get(Stat.Type.ARCHERY).getLevel();

		total /= 2;

		total += this.get(Stat.Type.HITPOINTS).getLevel();

		return total / 3;
	}

	protected synchronized int calcSkillTotal() {
		int total = 0;

		for (Stat stat : this)
			total += stat.getLevel();

		return total;
	}

	@Override
	public synchronized void update(Observable o, Object arg) {
		boolean changedLevel = (Boolean) arg;
		// Check if the level changed, or just the experience
		if (!changedLevel)
			return;

		// Something has changed, update the combat level and skill total
		combatLevel = this.calcCombatLevel();
		skillTotal = this.calcSkillTotal();
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		this.update(null, true); // Call a fake update to calculate the combat level and skill total
	}
} public abstract class Model extends Pickable {

	protected final Graphics graphics;
	protected final boolean animated;

	protected float scale;
	protected int rotation;
	protected Mesh[] meshes;

	public Model(Graphics graphics, boolean animated) {
		this.graphics = graphics;
		this.animated = animated;

		this.setScale(1);
		this.setRotation(0);

		meshes = new Mesh[0];
	}
	
	public boolean isAnimated() {
		return animated;
	}

	public float getScale() {
		return scale;
	}

	public void setScale(float scale) {
		this.scale = scale;
	}

	public int getRotation() {
		return rotation;
	}

	public void setRotation(int rotation) {
		rotation += 180;
		this.rotation = 360 - (rotation % 360);
	}

	protected void init() {
		// TODO: Load this model into a vertex buffer, so the only way to draw is via the buffer
	}

	public abstract void update();

	@Override
	protected void draw() {
		for (Mesh mesh : meshes) {
			// Set the texture for this mesh
			Material material = mesh.getMaterial();
			if (material != null) {
				GL11.glMaterial(GL11.GL_FRONT, GL11.GL_AMBIENT, material.ambient);
				GL11.glMaterial(GL11.GL_FRONT, GL11.GL_DIFFUSE, material.diffuse);
				GL11.glMaterial(GL11.GL_FRONT, GL11.GL_SPECULAR, material.specular);
				GL11.glMaterial(GL11.GL_FRONT, GL11.GL_EMISSION, material.emissive);
				GL11.glMaterialf(GL11.GL_FRONT, GL11.GL_SHININESS, material.shininess);

				// If we have a texture, bind it
				if (material.colorMap != null)
					GL11.glBindTexture(GL11.GL_TEXTURE_2D, material.colorMap.textureID);
			}

			GL11.glBegin(GL11.GL_TRIANGLES);
			{
				for (Triangle triangle : mesh.triangles) {
					for (Triangle.Point point : triangle.points) {
						GL11.glNormal3f(point.normal.x, point.normal.y, point.normal.z);
						GL11.glTexCoord2f(point.vertex.texturePosition.x, point.vertex.texturePosition.y);

						// If there is no bone, just draw the vertex
						Joint joint = point.vertex.getJoint();
						if (joint == null || !animated)
							GL11.glVertex3f(point.vertex.position.x, point.vertex.position.y, point.vertex.position.z);
						// Otherwise calculate the animation and draw
						else {
							Vector3f animationVector = joint.finalMatrix.vectorRotate(point.vertex.position); // TODO: or joint.position?

							animationVector.x += joint.finalMatrix.m03;
							animationVector.y += joint.finalMatrix.m13;
							animationVector.z += joint.finalMatrix.m23;

							GL11.glVertex3f(animationVector.x, animationVector.y, animationVector.z);
						}
					}
				}
			}
			GL11.glEnd();

			// Unbind the texture
			GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
		}
	}

	@Override
	public String toString() {
		return "model[meshes = " + meshes.length + "]";
	}
} public class MilkShapeModel extends AbstractMilkShapeModel {

	public static final String ID = "MS3D000000";
	public static final int VERSION = 4;

	protected float animationFPS;
	protected float currentTime;

	public MilkShapeModel(InputStream in, Graphics graphics) throws IOException {
		super(graphics);

		animationFPS = 0;
		currentTime = 0;

		LittleEndianDataInputStream data = new LittleEndianDataInputStream(new BufferedInputStream(in));

		// Confirm the file header is valid
		this.verifyHeader(data);

		super.meshes = this.parseMeshes(data);

		// Parse the materials
		materials = this.parseMaterials(data);

		animationFPS = data.readFloat();
		currentTime = data.readFloat();

		super.numFrames = data.readInt();

		// Parse the bone data
		joints = this.parseJoints(data);

		// There is some extra data next, but it's extra so lets ignore it unless we decide we need it

		// Update the material and joint reference in all meshes
		super.init();
	}

	private void verifyHeader(LittleEndianDataInputStream data) throws IOException {
		// Load the ID field and confirm it matches
		String identifier = data.readString(10);
		if (!ID.equals(identifier))
			throw new IOException("Attempted to load an unrecognised model format: " + identifier);

		int version = data.readInt();
		if (version != VERSION)
			throw new IOException("Attempted to load unsupported model version: " + version);
	}

	private Mesh[] parseMeshes(LittleEndianDataInputStream data) throws IOException {
		int numVertices = data.readUnsignedShort();
		Vertex[] vertices = new Vertex[numVertices];
		for (int i = 0;i < numVertices;i++) {
			int flags = data.readByte();
			Vector3f position = new Vector3f(
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			);

			int boneIndex = data.readByte();

			// Skip "reference count"
			for (int skip = 1, skipped = 0;skipped < skip;)
				skipped += data.skipBytes(skip - skipped);

			vertices[i] = new Vertex(
				flags,
				position,
				new Vector2f(), // Set later on
				boneIndex
			);
		}

		int numTriangles = data.readUnsignedShort();
		Triangle[] triangles = new Triangle[numTriangles];
		for (int i = 0;i < numTriangles;i++) {
			int flags = data.readUnsignedShort();

			int[] vertexIndices = new int[3];
			for (int j = 0;j < 3;j++)
				vertexIndices[j] = data.readUnsignedShort();

			Vector3f[] normals = new Vector3f[3];
			for (int j = 0;j < 3;j++) {
				normals[j] = new Vector3f(
					data.readFloat(),
					data.readFloat(),
					data.readFloat()
				);
			}

			Vector2f[] texturePosition = new Vector2f[3];
			for (int j = 0;j < 3;j++)
				texturePosition[j] = new Vector2f();

			for (int j = 0;j < 3;j++)
				texturePosition[j].x = data.readFloat();

			for (int j = 0;j < 3;j++)
				texturePosition[j].y = data.readFloat();

			Triangle.Point[] points = new Triangle.Point[3];
			for (int j = 0;j < 3;j++) {
				Vertex vertex = vertices[vertexIndices[j]];
				vertex.texturePosition.x = texturePosition[j].x;
				vertex.texturePosition.y = texturePosition[j].y;

				points[j] = new Triangle.Point(
					vertex,
					normals[j]
				);
			}

			int smoothingGroup = data.readByte();

			// Skip "group index"
			for (int skip = 1, skipped = 0;skipped < skip;)
				skipped += data.skipBytes(skip - skipped);

			triangles[i] = new Triangle(
				flags,
				points,
				smoothingGroup
			);
		}

		int numMeshes = data.readUnsignedShort();
		Mesh[] meshes = new Mesh[numMeshes];

		for (int i = 0;i < numMeshes;i++) {
			int flags = data.readByte();

			byte[] buffer = new byte[32];
			data.readFully(buffer, 0, buffer.length);
			String name = new String(buffer).trim();

			int _numTriangles = data.readUnsignedShort();
			Triangle[] _triangles = new Triangle[_numTriangles];
			for (int j = 0;j < _numTriangles;j++)
				_triangles[j] = triangles[data.readUnsignedShort()];

			int materialIndex = data.readByte();

			meshes[i] = new Mesh(name, flags, materialIndex, _triangles);
		}

		return meshes;
	}

	private Material[] parseMaterials(LittleEndianDataInputStream data) throws IOException {
		int numMaterials = data.readUnsignedShort();
		Material[] materials = new Material[numMaterials];

		for (int i = 0;i < numMaterials;i++) {
			String name = data.readString(32);
			if (name.isEmpty())
				throw new IOException("Found no-named material.");

			float[] ambient = {
				data.readFloat(),
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			};

			float[] diffuse = {
				data.readFloat(),
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			};

			float[] specular = {
				data.readFloat(),
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			};

			float[] emissive = {
				data.readFloat(),
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			};

			float shininess = data.readFloat();
			float transparency = data.readFloat();

			// Skip "mode"
			for (int skip = 1, skipped = 0;skipped < skip;)
				skipped += data.skipBytes(skip - skipped);

			// Load the color map texture, if there is one
			Sprite colorMap = null;
			String colorMapName = data.readString(128);
			if (!colorMapName.isEmpty()) {
				URL resource = Model.class.getResource(colorMapName);
				if (resource == null)
					throw new IOException("Unable to find color map resource: " + colorMapName);

				colorMap = graphics.loadSprite(resource);
			}

			// Load the alpha map texture, if there is one
			Sprite alphaMap = null;
			String alphaMapName = data.readString(128);
			if (!alphaMapName.isEmpty()) {
				URL resource = Model.class.getResource(alphaMapName);
				if (resource == null)
					throw new IOException("Unable to find alpha map resource: " + alphaMapName);

				alphaMap = graphics.loadSprite(resource);
			}

			materials[i] = new Material(name, ambient, diffuse, specular, emissive, shininess, transparency, colorMap, alphaMap);
		}

		return materials;
	}

	private Joint[] parseJoints(LittleEndianDataInputStream data) throws IOException {
		int numJoints = data.readShort();
		Map<String, Joint> joints = new HashMap<String, Joint>();

		for (int i = 0;i < numJoints;i++) {
			int flags = data.readByte();

			String name = data.readString(32);
			if (name.isEmpty())
				throw new IOException("Found no-named joint.");

			String parentName = data.readString(32);
			Joint parent = null;

			if (!parentName.isEmpty()) {
				if (!joints.containsKey(parentName))
					throw new IOException("Unable to find parent for joint: " + name);

				parent = joints.get(parentName);
			}

			Vector3f rotation = new Vector3f(
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			);

			Vector3f position = new Vector3f(
				data.readFloat(),
				data.readFloat(),
				data.readFloat()
			);

			int numRotationKeyframes = data.readShort();
			int numPositionKeyframes = data.readShort();

			Keyframe[] rotationKeyframes = new Keyframe[numRotationKeyframes];
			for (int j = 0;j < numRotationKeyframes;j++) {
				rotationKeyframes[j] = new Keyframe(
					data.readFloat(),		// time
					new Vector3f(
						data.readFloat(),	// x
						data.readFloat(),	// y
						data.readFloat()	// z
					)
				);
			}

			Keyframe[] positionKeyframes = new Keyframe[numPositionKeyframes];
			for (int j = 0;j < numPositionKeyframes;j++) {
				positionKeyframes[j] = new Keyframe(
					data.readFloat(),		// time
					new Vector3f(
						data.readFloat(),	// x
						data.readFloat(),	// y
						data.readFloat()	// z
					)
				);
			}

			joints.put(name, new Joint(name, parent, flags, position, rotation, positionKeyframes, rotationKeyframes));
		}

		return joints.values().toArray(new Joint[joints.size()]);
	}
}
import com.game.client.engine.GameEngine;
import com.game.client.model.Player;
import com.game.client.ui.ClientFrame;
import com.game.common.codec.Packet;
import com.game.common.codec.PacketBuilder;
import com.game.common.model.Friend;
import com.game.common.model.Hash;
import com.game.common.model.Inventory;
import com.game.common.model.Path;
import com.game.common.model.PlayerProfile;
import com.game.common.util.FriendList;
import com.game.common.util.StatList;
import com.game.graphics.models.Model;
import com.game.graphics.models.ms3d.MilkShapeModel;
package com.game.client;


public class WorldManager extends GameEngine {

	protected final Connection connection;

	protected PlayerProfile profile;

	public WorldManager(Client client) {
		super (client);

		connection = client.getConnection();
	}

	public Hash getID() {
		return profile.id;
	}

	public String getUsername() {
		return profile.username;
	}

	public FriendList getFriends() {
		return profile.friends;
	}

	public StatList getStats() {
		return profile.stats;
	}

	public Inventory getInventory() {
		return profile.inventory;
	}

	public void init(PlayerProfile profile) {
		this.profile = profile;

		client.setUpdateRate(ClientFrame.UPDATE_RATE_MAX);

		Model model = client.getGraphics().loadModel("test.ms3d", MilkShapeModel.class);
		model.setScale(0.03f);

		super.init(new Player(profile.id, profile.username, profile.location, model));
	}

	public void sendFriendAdd(String username) {
		PacketBuilder packet = new PacketBuilder(Packet.Type.FRIEND_ADD_SEND);