/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * 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 3 of the License, or (at
 * your option) any later version.
 * </p>
 * <p>
 * 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.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock,theys
 */

package org.starhope.appius.user;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.nio.channels.SocketChannel;
import java.sql.*;
import java.sql.Date;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.NamingException;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.*;
import org.starhope.appius.game.*;
import org.starhope.appius.game.inventory.*;
import org.starhope.appius.mb.Enrolment;
import org.starhope.appius.mb.UserEnrolment;
import org.starhope.appius.messaging.Mail;
import org.starhope.appius.sys.op.FilterResult;
import org.starhope.appius.sys.op.FilterStatus;
import org.starhope.appius.types.*;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.appius.via.RemoteUser;
import org.starhope.appius.via.ViaAppia;
import org.starhope.util.LibMisc;

import com.tootsville.user.Toot;
import com.whirlycott.cache.Cache;

/**
 * This class encapsulates all of the user/player information for the
 * game.
 * 
 * @author brpocock
 * @author theys
 */
public abstract class User extends Person implements AbstractUser,
ViaAppia <RemoteUser>, Comparable <User>, HasVariables {

	/**
	 * A simple cache of live users
	 */
	private static final ConcurrentHashMap <String, User> liveCache = new ConcurrentHashMap <String, User> ();

	/**
	 * Maximum length of a login/username (in characters)
	 */
	public final static int MAX_LOGIN_LENGTH = 30;

	/**
	 * Maximum length of a password (in characters)
	 */
	public final static int MAX_PW_LENGTH = 30;
	/**
	 * Minimum length of a login/username (in characters)
	 */
	public final static int MIN_LOGIN_LENGTH = 3;

	/**
	 * Minimum length of a password (in characters)
	 */
	public final static int MIN_PW_LENGTH = 3;

	/**
	 * 
	 */
	private static int serial = -1;
	/**
	 * Serial Version Unique Identifier for Java Serialization.
	 * serialVersionUID (long)
	 */
	private static final long serialVersionUID = 4146069318927248889L;
	/**
	 * The Smart Fox Server that we might be able to talk to.
	 */
	// private static SmartFoxServer sfs = null;
	/**
	 * Staff level for users able to view account (subscription) data
	 */
	public static final int STAFF_LEVEL_ACCOUNT_SERVICE = 3;

	/**
	 * Staff level for users able to edit the game world
	 */
	public static final int STAFF_LEVEL_DESIGNER = 4;
	/**
	 * Staff level for software developers
	 */
	public final static int STAFF_LEVEL_DEVELOPER = 8;
	/**
	 * Staff level for moderators (including life guards and tour
	 * guides)
	 */
	public static final int STAFF_LEVEL_MODERATOR = 2;

	/**
	 * Staff level for public users (free or paid users)
	 */
	public static final int STAFF_LEVEL_PUBLIC = 0;

	/**
	 * Staff level for all members of the staff (Sidereal employees)
	 */
	public static final int STAFF_LEVEL_STAFF_MEMBER = 1;

	/**
	 * The WhirleyCache of users
	 */
	private static Cache userCache = AppiusConfig
	.getCache ("UserCache");

	/**
	 * <pre>
	 * theys Jan 7, 2010
	 * </pre>
	 * 
	 * TO __debugTracePeanuts trace the stack and display the amount of
	 * peanuts
	 * 
	 * @param me The user being debugged.
	 */
	protected static void __debugTracePeanuts (final AbstractUser me) {
		if (me instanceof Toot
				&& "mouser".equals (me.getAvatarLabel ())) {
			AppiusClaudiusCaecus.traceThis ("I am "
					+ me.getAvatarLabel () + " I am a "
					+ me.getClass ().getCanonicalName ()
					+ "  I have this many peanuts --> "
					+ ((Toot) me).getPeanuts ().toPlainString ());
		}
	}

	/**
	 * <p>
	 * Make the assertion that the user name supplied is available to be
	 * requested or assigned to this user.
	 * </p>
	 * <p>
	 * Note that having another user request the name, which has not
	 * been either permitted or denied, will still throw an
	 * AlreadyUsedException.
	 * </p>
	 * <p>
	 * This routine returns void, because it throws exceptions if the
	 * name is forbidden or already used. For a boolean version, see
	 * {@link #isNameAvailable(String)}
	 * 
	 * @param userNameRequested The name which is being requested
	 * @throws AlreadyUsedException if the user name has been requested
	 *             or accepted already
	 * @throws ForbiddenUserException if the user name is forbidden from
	 *             use (obscene, gives away personal information, or so
	 *             forth). See {@link #isNameForbidden(String)}
	 */
	public static void assertUserNameAvailable (
			final String userNameRequested)
	throws AlreadyUsedException, ForbiddenUserException {

		if (null == userNameRequested || "".equals (userNameRequested))
			throw new ForbiddenUserException (userNameRequested);

		if ( !userNameRequested.startsWith ("$loadclient.")
				&& User.isNameForbidden (userNameRequested))
			throw new ForbiddenUserException (userNameRequested);

		AbstractUser gotItFirst = User.getByLogin (userNameRequested);
		if (null != gotItFirst)
			throw new AlreadyUsedException (userNameRequested,
					gotItFirst.getNameApprovedAt ());

		gotItFirst = User.getByRequestedName (userNameRequested);
		if (null != gotItFirst)
			throw new AlreadyUsedException (userNameRequested,
					gotItFirst.getNameRequestedAt ());

		return;
	}

	/**
	 * Check whether the SmartFox Server is available and visible to us
	 * (sharing this VM). If so, the static member variable "sfs" will
	 * be set to it. Since this is not longer supported, the results
	 * will always be false and this method is deprecated.
	 * 
	 * @return true, if we have access to Smart Fox Server.
	 */
	@Deprecated
	public static boolean canSeeSmartFoxServer () {
		return false;
	}

	/**
	 * This is the callback that is called whenever the AppiusConfig
	 * reloads the configuration or has a runtime configuration value
	 * changed. Since User class can use a Whirley cache, this fetches a
	 * new instance.
	 */
	public static void configUpdated () {
		User.userCache = AppiusConfig.getCache ("User");
	}

	/**
	 * Create a new user account
	 * 
	 * @param date User's date of birth
	 * @param string Character class or type designator
	 * @param nick User's requested nickname
	 * @return the new user object
	 * @throws AlreadyUsedException if the nickname is not available
	 * @throws ForbiddenUserException if the user account is not
	 *             permitted to be created, e.g. for having an obscene
	 *             user ID
	 */
	public static User create (final Date date, final String string,
			final String nick)
	throws AlreadyUsedException, ForbiddenUserException {
		try {
			return AppiusConfig.getUserClass ().getConstructor (
					Date.class, String.class, String.class)
					.newInstance (date, string, nick);
		} catch (final IllegalArgumentException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a IllegalArgumentException in create", e);
		} catch (final SecurityException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a SecurityException in create", e);
		} catch (final InstantiationException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a InstantiationException in create", e);
		} catch (final IllegalAccessException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a IllegalAccessException in create", e);
		} catch (final InvocationTargetException e) {
			final Throwable cause = e.getCause ();
			if (cause instanceof AlreadyUsedException)
				throw (AlreadyUsedException) cause;
			if (cause instanceof ForbiddenUserException)
				throw (ForbiddenUserException) cause;
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a InvocationTargetException in create", e);
		} catch (final NoSuchMethodException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a NoSuchMethodException in create", e);
		}
	}

	/**
	 * Delete test users: test???? accounts with email of
	 * testing@tootsville.com will be DESTROYED from the database.
	 * 
	 * @throws SQLException if it can't be done.
	 */
	public static void dangerous__removeTestUsers ()
	throws SQLException {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("DELETE FROM users WHERE userName LIKE 'test____' AND mail LIKE '%@tootsville.com';");
			st.executeUpdate ();
			AppiusClaudiusCaecus
			.blather ("Deleting test users (test____ / @tootsville.com\n Deleted records: "
					+ st.getUpdateCount ());
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * Print out a nasty, verbose debugging dump of the users live cache
	 * to the STDOUT. Toggled on using configuration variable
	 * org.starhope.appius.dumpLiveCache
	 */
	private static void dumpLiveCache () {
		if (AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
			System.out
			.println ("liveCache\t______________________________");
			for (final Entry <String, User> entry : User.liveCache
					.entrySet ()) {
				System.out.println ("liveCache\t" + entry.getKey ()
						+ "\tfrom " + entry.getValue ().getIPAddress ()
						+ "\tin " + entry.getValue ().getRoom ());
			}
			System.out
			.println ("liveCache\t^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
		}
	}

	/**
	 * Pick up a user from a JSON object containing either the ID or
	 * login (user name) string
	 * 
	 * @param object A JSON object with either an { id: userID } or {
	 *            login: userName }
	 * @return the User thusly fetched
	 */
	public static AbstractUser get (final JSONObject object) {
		if (object.has ("id")) {
			try {
				return User.getByID (object.getInt ("id"));
			} catch (final JSONException e) {
				// No op
			}
		}
		if (object.has ("login")) {
			try {
				return User.getByLogin (object.getString ("login"));
			} catch (final JSONException e) {
				// No op
			}
		}
		return null;
	}

	/**
	 * WRITEME: Document this field. theys Dec 16, 2009
	 * 
	 * @param typeID WRITEME
	 * @param userID WRITEME
	 * @return WRITEME
	 */
	public static InventoryItem getActiveItemByTypeAndUserID (
			final int typeID, final int userID) {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT slot, x, y, facing, color, ID, isActive, itemTypeID AS typeID, userID, name, filename, description, portrait, value FROM inventory LEFT JOIN items ON inventory.itemID=items.ID WHERE userID=? AND isActive='Y' AND itemTypeID=? LIMIT 1");
			st.setInt (1, userID);
			st.setInt (2, typeID);
			rs = st.executeQuery ();
			if (rs.next ()) return InventoryItem.getFromInventory (rs);
			return null;
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"Cannot get active item by user id", e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		return null;
	}

	/**
	 * WRITEME: Document this field. theys Dec 16, 2009
	 * 
	 * WRITEME
	 * 
	 * <p>
	 * BRP: I believe the idea here is to bypass actually instantiating
	 * an user object just to acquire one's inventory
	 * </p>
	 * 
	 * @param type The item type
	 * @param userID the user whose inventory we want
	 * @return the first item of that type that the user has equipped
	 */
	public static InventoryItem getActiveItemByTypeAndUserID (
			final InventoryItemType type, final int userID) {
		return User
		.getActiveItemByTypeAndUserID (type.getID (), userID);
	}

	/**
	 * Instantiate a user object from an existing user account ID
	 * 
	 * @param id The user ID to instantiate
	 * @return the instantiated user record, or null if the user ID
	 *         doesn't represent a user record (too high, or the record
	 *         was destroyed somehow) — not returned for deleted or
	 *         banned accounts, though.
	 */
	public static AbstractUser getByID (final int id) {
		if (null != User.liveCache) {
			User.dumpLiveCache ();
			final User u = User.liveCache.get ("uid=" + id);
			if (null != u) {
				if (AppiusConfig
						.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
					System.out.println ("livecache\tfound uid=" + id
							+ " in liveCache, S#" + u.getMySerial ());
				}
				return u;
			}
			if (AppiusConfig
					.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
				System.out.println ("livecache\tdid not find uid=" + id
						+ " in liveCache");
			}
		}
		if (null != User.userCache) {
			final Object o = User.userCache.retrieve ("uid=" + id);
			if (null != o) {
				if ( !o.getClass ().equals (User.class)) {
					User u = null;
					try {
						u = (User) o;
					} catch (final ClassCastException e) {
						AppiusClaudiusCaecus
						.reportBug ("Class Cast Fuck");
					}
					if (null != u) return u;
				}
			}
		}
		PreparedStatement st = null;
		Connection con = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM users WHERE ID=?");
			st.setInt (1, id);
			if ( !st.execute ()) {
				AppiusClaudiusCaecus
				.reportBug ("Tried to fetch User by ID " + id
						+ " and failed");
				return null;
			}
			rs = st.getResultSet ();
			if ( !rs.next ()) return null;
			return User.instantiateUser (rs);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			return null;
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * @param login the user login name
	 * @return the User record, or null if no user <em>currently</em>
	 *         has that login name
	 */
	public static AbstractUser getByLogin (final String login) {
		if (login.charAt (0) == '#') {
			try {
				return User.getByID (Integer.parseInt (login
						.substring (1)));
			} catch (final NumberFormatException e) {
				return null;
			}
		}
		if (null != User.liveCache) {
			User.dumpLiveCache ();
			final User foundUser = User.liveCache.get ("log="
					+ login.toLowerCase (Locale.ENGLISH));
			if (null != foundUser) {
				if (AppiusConfig
						.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
					System.out.println ("livecache\tfound log=" + login
							+ " in liveCache, S#"
							+ foundUser.getMySerial ());
				}
				return foundUser;
			}
			if (AppiusConfig
					.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
				System.out.println ("livecache\tdid not find log="
						+ login + " in liveCache");
			}
		}
		if (null != User.userCache) {
			final Object o = User.userCache.retrieve ("log=" + login);
			if (null != o) {
				if ( !o.getClass ().equals (User.class)) {
					User u = null;
					try {
						u = (User) o;
					} catch (final ClassCastException e) {
						System.err.println ("Class Cast Fuck");
					}
					if (AppiusConfig.isDebug ()) {
						System.out.println ("Found user “" + login
								+ "” in cache");
					}
					if (null != u) return u;
				}
			}
		}
		PreparedStatement st = null;
		Connection con = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM users WHERE userName=?");
			st.setString (1, login);
			if ( !st.execute ()) {
				AppiusClaudiusCaecus
				.reportBug ("Tried to fetch User by Login "
						+ login + " and failed");
				return null;
			}
			rs = st.getResultSet ();
			if ( !rs.next ()) return null;
			return User.instantiateUser (rs);
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
			return null;
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * Returns an array of all users associated with a given eMail
	 * address. This includes all users who report it as being their own
	 * eMail address, as well as the children of any parents using it.
	 * 
	 * @param mail The eMail address for which we are searching
	 * @return An array of any/all such users. If the array consists
	 *         only of one element, which is the value "null," then
	 *         there are too many results and special effort is required
	 *         to recall the list.
	 */
	public static AbstractUser [] getByMail (final String mail) {
		final AbstractUser [] them = {};
		Connection con = null;
		PreparedStatement getUsers = null;
		PreparedStatement getParents = null;
		ResultSet usersSet = null;
		ResultSet parentsSet = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			getUsers =
				con
				.prepareStatement ("SELECT * FROM users WHERE mail = ?");
			getUsers.setString (1, mail);
			if (getUsers.execute ()) {
				usersSet = getUsers.getResultSet ();
				while (usersSet.next ()) {
					them [them.length] =
						User.getByLogin (usersSet
								.getString ("userName"));
				}
			}

			getParents =
				con
				.prepareStatement ("SELECT * FROM users LEFT JOIN parents WHERE users.parentID = parents.id AND parents.mail = ?");
			getParents.setString (1, mail);
			if (getParents.execute ()) {
				parentsSet = getParents.getResultSet ();
				while (parentsSet.next ()) {
					them [them.length] =
						User.getByLogin (parentsSet
								.getString ("userName"));
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != usersSet) {
				try {
					usersSet.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != getUsers) {
				try {
					getUsers.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != parentsSet) {
				try {
					parentsSet.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != getParents) {
				try {
					getParents.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		return them;
	}

	/**
	 * Get the user who has requested a certain name, if any.
	 * 
	 * @param userNameRequested the user name for which we're searching
	 * @return null, if no user has requested the name; otherwise, the
	 *         user who requested it. (Note that the name might have
	 *         been approved, or might not have been.)
	 */
	public static User getByRequestedName (
			final String userNameRequested) {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM users WHERE requestedName=?");

			st.setString (1, userNameRequested);
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) {
					final User u = User.instantiateUser (rs);
					return u;
				}
				return null;
			}
		} catch (final SQLException e) {
			return null;
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return null;
	}

	/**
	 * Find the user name for a user who is currently signed on.
	 * 
	 * @param name User name to look up
	 * @return The user ID number, if the user is online; else -1
	 */
	public static int getIDForLiveUserName (final String name) {
		if (null != User.liveCache) {
			User.dumpLiveCache ();
			final User u = User.liveCache.get ("log="
					+ name.toLowerCase (Locale.ENGLISH));
			if (null != u) return u.getUserID ();
		}
		if (null != User.userCache) {
			final User u = (User) User.userCache.retrieve ("log="
					+ name);
			if (null != u) return u.getUserID ();
		}
		return -1;
	}

	/**
	 * Fetch the user ID number for a user name
	 * 
	 * @param name The user name (login)
	 * @return the user ID
	 */
	public static int getIDForLogin (final String name) {
		final int liveID = User.getIDForLiveUserName (name);
		if (liveID > 0) return liveID;
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT ID FROM users WHERE userName=?");

			st.setString (1, name);
			if (st.execute ()) {
				rs = st.getResultSet ();
				rs.next ();
				final int theirID = rs.getInt ("ID");
				return theirID;
			}
		} catch (final SQLException e) {
			// no op
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return 0;
	}

	/**
	 * Fetch the user ID number for a user name
	 * 
	 * @param name The user name (login)
	 * @return the user ID
	 * @deprecated {@link User#getIDForLogin(String)}
	 */
	@Deprecated
	public static int getIDForUserName (final String name) {
		return User.getIDForLogin (name);
	}

	/**
	 * @return the Subversion revision number of this file
	 */
	public static String getRev () {
		return "$Rev: 2601 $";
	}

	/**
	 * Get the System user object (the user which represents the system
	 * program itself). In particular, the System object's eMail address
	 * and givenName are used to address mail to users.
	 * 
	 * @return the System user object
	 */
	public static AbstractUser getSystemUser () {
		return User.getByID (1);
	}

	/**
	 * @param id The user ID value
	 * @return The user's login name
	 */
	public static String getUserNameForID (final int id) {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT userName FROM users WHERE ID=?");

			st.setInt (1, id);
			if (st.execute ()) {
				rs = st.getResultSet ();
				rs.next ();
				final String userName = rs.getString ("userName");

				return userName;
			}
		} catch (final SQLException e) {
			/* no op */
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		return "(user " + id + ")";
	}

	/**
	 * Get up to 20 users who are awaiting approval of their names. Will
	 * not return more than 20 users in a set, but could return an empty
	 * set.
	 * 
	 * @return A set of users who are awaiting name approval
	 */
	public static Collection <AbstractUser> getUsersAwaitingNameApproval () {
		final Vector <AbstractUser> them = new Vector <AbstractUser> ();
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con.prepareStatement ("SELECT * FROM users LIMIT 20");
			if (st.execute ()) {
				rs = st.getResultSet ();
				while (rs.next ()) {
					them.add (User.instantiateUser (rs));
				}
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return them;
	}

	/**
	 * Z$Z
	 * 
	 * @return Z$Z
	 */
	public static String [] getZ$Z () {
		return new String [] {
				"Fcrpvny Gunaxf Gb\nPuevf Oheyvatunz, Wbpryla Jvafybj, Tnoevry Jvafybj, Pbql Gnsg, Znel Oynwvna naq rirelbar ryfr!",
				"Onfrq hcba 2008 Gbbgfivyyr™ Sebag-raq “Nzcuvovbhf” ol:\n\tPuevf Naqrefba\n\tOenaqba Obbxre\n\tWba Onegyrg\n\tOelna Pbql\n\tOreenr Zrvkfryy\nNaq freire “Avtugzner” ol:\n\tZnex Zp Pbexyr\n\tPney Avpby\n\tOehpr-Eboreg Cbpbpx\n",
				"Gbbgfivyyr Sebag-raq “Crefrcubar” ol\n\tEboreg Qnjfba\n\n“Nccvhf Pynhqvhf Pnrphf” tnzr freire ol\n\tOehpr-Eboreg Cbpbpx\n\tCbegvbaf © 2008-2010, Oehpr-Eboreg Cbpbpx\n\t\tyvprafrq haqre gur TAH TCY\n\nZrzorefuvc+Ovyyvat naq GbbgfObbx ol\n\tGvz Urlf\n",
				"Flfgrzf Nqzvavfgengvba\tTrar Pebax\nBcrengvbaf Znantre\tRzvyl Gnsg\nOvyyvat Znantre\t\t\tRzvyl Qnjfba\nUrnq Yvsrthneq\t\tEvpuneq Uneaqra\nNeg Qverpgbe\t\t\tWbua Zp Xvayrl\nYrnq Navzngbe\t\t\tFrna Xvat\n",
		"\t»\tGbbgfivyyr™\t«\n\nPbclevtug © 2007-2010, Erf Vagrenpgvir, YYP\nNyy Evtugf Erfreirq\n\nQverpgbe\tYbhvf Crppv\nGrpuavpny Qverpgbef\tPuevf Oehaare+Nyrk Crppv\nYvar Cebqhpre\tYneel Oretre\nZnexrgvat\t\tXevfgv Qhaa, Wblpr Crppv, naq Obo Onlqnyr\n" };
	}

	/**
	 * Instantiate a User (using the selected subclass) from the result
	 * set garnered from a SELECT *
	 * 
	 * @param rs The given database record
	 * @return The User (subclass) representing that database record
	 */
	private static User instantiateUser (final ResultSet rs) {
		try {
			final Class <? extends User> userSubclass = AppiusConfig
			.getUserClass ();
			if (null == userSubclass)
				throw AppiusClaudiusCaecus
				.fatalBug ("User class is not defined.");
			final Constructor <? extends User> constructor = userSubclass
			.getConstructor (ResultSet.class);
			if (null == constructor)
				throw AppiusClaudiusCaecus
				.fatalBug ("Can't find <init>(ResultSet) constructor in "
						+ userSubclass.getCanonicalName ());
			final User newInstance = constructor.newInstance (rs);
			return newInstance;
		} catch (final IllegalArgumentException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (
					"Caught a IllegalArgumentException in instantiateUser",
					e);
		} catch (final SecurityException e) {
			throw AppiusClaudiusCaecus.fatalBug (
					"Caught a SecurityException in instantiateUser", e);
		} catch (final InstantiationException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (
					"Caught a InstantiationException in instantiateUser",
					e);
		} catch (final IllegalAccessException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (
					"Caught a IllegalAccessException in instantiateUser",
					e);
		} catch (final InvocationTargetException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (
					"Caught a InvocationTargetException in instantiateUser",
					e);
		} catch (final NoSuchMethodException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (
					"Caught a NoSuchMethodException in instantiateUser",
					e);
		}
	}

	/**
	 * Perform basic self-tests upon the User database to identify
	 * whether things are good enough to proceed with booting.
	 */
	public static void isItGood () {
		User god = (User) User.getByID (1);
		User lah = (User) User.getByID (1);
		if (god.getSerial () != lah.getSerial ())
			throw AppiusClaudiusCaecus
			.fatalBug ("User cache corruption or failure in getByID.");
		User el = (User) User.getByLogin (god.getLogin ());
		if (god.getSerial () != el.getSerial ())
			throw AppiusClaudiusCaecus
			.fatalBug ("User cache corruption or failure in getByLogin");
		if (god instanceof Toot) {
			Toot deus = (Toot) god;
			final BigDecimal godsNuts = deus.getPeanuts ();
			/*
			 * The following lines generate somewhat spurious warnings
			 * from Find Bugs, but the idea is to remove God from being
			 * a live user. This will only work running stand-alone,
			 * which at this point, we should be.
			 */
			deus = null;
			god = null;
			lah = null;
			el = null;
			Runtime.getRuntime ().gc ();
			final Toot dieu = (Toot) User.getByID (1);
			try {
				dieu.giftPeanuts (BigDecimal.TEN, "givenuts");
			} catch (final AlreadyExistsException e) {
				throw AppiusClaudiusCaecus.fatalBug (
						"Caught a AlreadyExistsException in isItGood",
						e);
			} catch (final JSONException e) {
				throw AppiusClaudiusCaecus.fatalBug (
						"Caught a JSONException in isItGood", e);
			}
			final BigDecimal godsNewNuts = dieu.getPeanuts ();
			if (godsNuts.add (BigDecimal.TEN).compareTo (godsNewNuts) != 0)
				throw AppiusClaudiusCaecus
				.fatalBug ("ADONAI eloheinu, ADONAI echad : "
						+ godsNuts.toPlainString () + "/"
						+ godsNewNuts.toPlainString ());
		}
	}

	/**
	 * Determine whether the given name is potentially available for
	 * use. Returns false if the name has already been forbidden (by
	 * virtue of matching a negative filter rule, or having been
	 * previously denied to another user), or if the name is currently
	 * either in use or requested by another user.
	 * 
	 * @param name The user name being checked
	 * @return true, if the name can potentially be tried.
	 */
	public static boolean isNameAvailable (final String name) {
		try {
			User.assertUserNameAvailable (name);
		} catch (final AlreadyUsedException e) {
			return false;
		} catch (final ForbiddenUserException e) {
			return false;
		}
		return true;
	}

	/**
	 * Determine whether a name is forbidden
	 * <p>
	 * ... A user name is “forbidden” if it matches a negative filter
	 * (if it contains forbidden word(s) or phrase(s)), or if it has
	 * previously been banned for some reason
	 * </p>
	 * 
	 * @param userNameRequested The name to be checked
	 * @return True, if the requested name is forbidden
	 */
	public static boolean isNameForbidden (
			final String userNameRequested) {

		if ( !User.isNameValid (userNameRequested)) {
			AppiusClaudiusCaecus.blather ("Name is invalid "
					+ userNameRequested);
			return true;
		}

		AppiusConfig.getFilter (FilterType.USER_LOGIN);

		String userNameNoNum = "";
		final Pattern noNums = Pattern.compile ("[0-9]");
		final Matcher m = noNums.matcher (userNameRequested);
		userNameNoNum = m.replaceAll ("");

		if (1 < userNameNoNum.length ()) {
			final FilterResult carlSays = Zone.censor
			.filterMessage (userNameNoNum);

			if (carlSays.status == FilterStatus.Black
					|| carlSays.status == FilterStatus.Red) {
				if (AppiusConfig
						.getConfigBoolOrFalse ("com.tootsville.showNameFilter")) {
					System.out.println (new Date (System
							.currentTimeMillis ())
					+ " ---- "
					+ userNameRequested
					+ "("
					+ userNameNoNum
					+ ") is forbidded.  Reason: "
					+ carlSays.toString ());
				}
				return true;
			}
		}

		return false;
	}

	/**
	 * Determines whether the name provided contains allowed characters
	 * for an user name.
	 * 
	 * @param userName the user name to be checked
	 * @return true, if the name consists of valid characters
	 */
	public static boolean isNameValid (final String userName) {
		final char first = userName.charAt (0);
		if ( ! (first >= 'a' && first <= 'z' || first >= 'A'
			&& first <= 'Z')) return false;

		int numbersInARow = 0;
		boolean wasPunctuation = false;

		for (final char ch : userName.toCharArray ()) {
			if ( ! (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z'
				|| ch >= '0' && ch <= '9' || ch == '.' || ch == '-'))
				return false;

			if (ch == '.' || ch == '-') {
				if (wasPunctuation) return false;
				wasPunctuation = true;
			} else {
				wasPunctuation = false;
			}

			if (ch >= '0' && ch <= '9') {
				++numbersInARow;
			} else {
				numbersInARow = 0;
			}
			if (numbersInARow > 3) return false;
		}

		return true;
	}

	/**
	 * The age bracket to which this user belongs: currently kid (0-12),
	 * teen (13-17), or adult (18+). Computed from birth date
	 * periodically and cached.
	 */
	private AgeBracket ageGroup;

	/**
	 * The date on which the account was approved (by parent) or eMail
	 * was validated (by self). Either way, it's basically eMail
	 * validation, but just differs in who gets the mail. The date on
	 * which the account was approved (by parent) or eMail was validated
	 * (by self). Either way, it's basically eMail validation, but just
	 * differs in who gets the mail.
	 */
	private Date approvedDate;

	/**
	 * The type of avatar in play
	 */
	private AvatarClass avatarClass;

	/**
	 * The base colour of the avatar
	 */
	private Colour baseColor;

	/**
	 * The user's date of birth. Required, COPPA.
	 */
	private Date birthDate;

	/**
	 * The list of buddies for this user. Instantiated on the fly by the
	 * getter methods, if needed, to save on overhead.
	 */
	private org.starhope.appius.types.UserList buddyList = null;

	/**
	 * True if the user can sign in to a Beta server
	 */
	private boolean canBetaTest;

	/**
	 * True if the user can enter a zone where people can chat freely.
	 * Lightning in Tootsville.
	 */
	private boolean canEnterChatZone;

	/**
	 * True if the user can enter a zone where dialogue is chosen from
	 * menus. Hearts in Tootsville.
	 */
	private boolean canEnterMenuZone;

	/**
	 * True if the user is allowed to type chat.
	 */
	private boolean canTalk;

	/**
	 * background colour for chat text
	 */
	private Colour chatBG = Colour.WHITE;

	/**
	 * foreground colour for chat text
	 */
	private Colour chatFG = Colour.BLACK;

	/**
	 * The current room in which the user is active. This can be null.
	 */
	private transient AbstractRoom currentRoom = null;

	/**
	 * For kids, this is the date on which eMail Plus secondary eMail is
	 * sent out. This may be past or future.
	 */
	private Date emailPlusDate;

	/**
	 * The extra colour of the avatar. For Master Toot (Amphibious) this
	 * is the nose/highlight colour
	 */
	private Colour extraColor;

	/**
	 * The direction in which the user is facing. Must be one of: N S E
	 * W NE NW SW SE
	 */
	private String facing = "S";

	/**
	 * Has the user's inventory been loaded?
	 */
	private boolean haveInventory = false;

	/**
	 * The list of users whom this user is ignoring.
	 */
	private UserList ignoreList = null;

	/**
	 * All types of inventory in a single set.
	 */
	private final HashSet <InventoryItem> inventory = new HashSet <InventoryItem> ();

	/**
	 * If true, the account is active (not canceled nor banned)
	 */
	private boolean isActive;

	/**
	 * If true, the account is banned (not active nor canceled)
	 */
	private boolean isBanned;

	/**
	 * If true, the account is canceled (not active nor banned)
	 */
	private boolean isCanceled;

	/**
	 * The user ID who kicked this user offline. (or -1)
	 */
	private int kickedByUserID;

	/**
	 * The reason that this user is kicked offline or banned.
	 */
	private String kickedReasonCode;

	/**
	 * The date at which the user is no longer kicked offline. For
	 * banned users, this is Timestamp ( Long.MAX_LONG ), which means
	 * the universe should end first.
	 */
	private Timestamp kickedUntil;

	/**
	 * the time at which the user last was logged on, or the current
	 * time (plus 5 seconds) if they're on now.
	 */
	private Timestamp lastActive;

	/**
	 * the last zone on which the user had been logged on; or, their
	 * current zone, if we can see Smartfox from here.
	 */
	private String lastZoneName;

	/**
	 * The user's current, active login name.
	 */
	private String login;

	/**
	 * unique instance ID
	 */
	private transient final int mySerial;

	/**
	 * The timestamp of Lifeguard approval of the user's name (or null,
	 * if it hasn't been approved yet)
	 */
	private Timestamp nameApprovedAt;

	/**
	 * The user ID of the moderator who approved this user's name
	 */
	private long nameApprovedByUserID;

	/**
	 * The time at which this user entered his/her request for a new
	 * name.
	 */
	protected Timestamp nameRequestedAt;

	/**
	 * If true, the user hasn't picked a name for this account (but it
	 * may have a system-assigned random one), so we need to nag them to
	 * pick a name.
	 */
	protected boolean needsNaming;

	/**
	 * Whether the user's parent has approved the name yet.
	 */
	private boolean parentApprovedName;

	/**
	 * Pointer to the parent of this user, if the user's age bracket is
	 * "kid"
	 */
	private int parentID;

	/**
	 * The 1-4 character code representing the source of a referral
	 * which resulted in this user signing up. Referrer (referer) codes
	 * are assigned in the M&B web site to “brand” users for life based
	 * upon who caused them to sign up with the site.
	 */
	protected String referer = "";
	/**
	 * Time when the user originally registered
	 */
	private Timestamp registeredAt = null;

	/**
	 * The user name which the user has requested, but has not yet been
	 * approved.
	 */
	protected String requestedName = "";

	/**
	 * The set of rooms in this user's house (and yard). Special values:
	 * 0 is the first room (everyone gets it for free), and 1 is the
	 * yard (lot).
	 */
	private final HashSet <Integer> rooms = new HashSet <Integer> ();

	/**
	 * The server thread through which this user is connected.
	 */
	private transient AppiusClaudiusCaecus serverThread;

	/**
	 * The level of staff authority possessed by this user.
	 */
	protected int staffLevel;

	/**
	 * The destination ordinate toward which the user is currently
	 * moving. (May be identical to {@link #x})
	 */
	private double targetX;

	/**
	 * The destination mantissa toward which the user is currently
	 * moving. (May be identical to {@link #y})
	 */
	private double targetY;

	/**
	 * The rate of movement (in pixels per millisecond) of this user.
	 */
	private double travelRate;

	/**
	 * The time at which the user started moving on their current
	 * movement vector. Measured in milliseconds since epoch.
	 */
	private long travelStartTime;

	/**
	 * The user's numeric ID, for database purposes.
	 */
	protected int userID;

	/**
	 * Arbitrary user variables which can be set or retrieved by the
	 * front-end
	 */
	private final ConcurrentHashMap <String, String> userVariables = new ConcurrentHashMap <String, String> ();

	/**
	 * The user's origination ordinate
	 */
	private double x;

	/**
	 * The user's origination abcessa
	 */
	private double y;

	/**
	 * Create a new user account
	 * 
	 * @param birthDate1 The player's date of birth
	 * @param avatarTitle The name of the avatar (class) which the
	 *            player wants to use. This must be one of the fixed
	 *            string names of the Basic 8 Toots™
	 * @param userNameRequest The user name requested
	 * @throws ForbiddenUserException If the user is administratively
	 *             prohibited from registering, e.g. due to a bad user
	 *             name
	 * @throws AlreadyUsedException If the user name is not available
	 * @throws NumberFormatException If the date of birth is irrational
	 */
	protected User (final Date birthDate1, final String avatarTitle,
			final String userNameRequest)
	throws AlreadyUsedException, ForbiddenUserException,
	NumberFormatException {
		userID = -1;

		mySerial = ++User.serial;
		AppiusClaudiusCaecus.blather ("Instantiated S#" + mySerial
				+ " as new user");

		if (birthDate1.before (java.sql.Date.valueOf ("1900-1-1")))
			throw new NumberFormatException ("Birth date out of range");
		if (birthDate1.after (new Date (System.currentTimeMillis ())))
			throw new NumberFormatException ("Birth date out of range");

		birthDate = birthDate1;
		if (canApproveSelf ()) {
			approvedDate = new Date (System.currentTimeMillis ());
			canEnterChatZone = true;
			canTalk = false;
			canEnterMenuZone = true;
		} else {
			approvedDate = null;
			canTalk = false;
			canEnterChatZone = false;
			canEnterMenuZone = true;
		}

		User.assertUserNameAvailable (userNameRequest);
		requestedName = userNameRequest;

		// Give param toot class in string format, ex. 'zap'
		avatarClass = new AvatarClass (avatarTitle);

		baseColor = avatarClass.getDefaultBaseColor ();
		extraColor = avatarClass.getDefaultExtraColor ();

		setAgeGroup ();

		insertIntoDatabase (birthDate1);

		setAgeGroup ();

		try {
			AppiusClaudiusCaecus.blather ("#" + userID, "", "",
					"New user created, database ID =  " + userID, true);
			populateByID (userID);
		} catch (final NotFoundException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}

		try {
			approveName (User.getByID (1));
		} catch (final PrivilegeRequiredException e1) {
			AppiusClaudiusCaecus.fatalBug (e1);
		}

		local_create ();
	}

	/**
	 * Instantiate a user object from an existing user account ID
	 * 
	 * @param id user ID value
	 * @throws NotFoundException if the user can't be found in the
	 *             database.
	 */
	public User (final int id) throws NotFoundException {
		mySerial = ++User.serial;
		AppiusClaudiusCaecus.blather ("Instantiated S#" + mySerial
				+ ", populating by ID");
		populateByID (id);
	}

	/**
	 * Instantiate a User object based upon the contents of a ResultSet.
	 * This is used internally by some of the static methods that return
	 * sets of users based upon database queries; e.g. see
	 * {@link #getUsersAwaitingNameApproval()}
	 * 
	 * @param rs The ResultSet object (with the cursor at the current
	 *            row to be instantiated). Note that this implies that
	 *            rs.next() has been called at least once.
	 */
	protected User (final ResultSet rs) {
		mySerial = ++User.serial;
		AppiusClaudiusCaecus.blather ("Instantiated S#" + mySerial
				+ " from ResultSet (private constructor)");
		try {
			this.set (rs);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
	}

	/**
	 * Instantiate a user object from an existing user account ID
	 * 
	 * @param newUserLogin user login name
	 * @throws NotFoundException if the user can't be found in the
	 *             database.
	 */
	public User (final String newUserLogin) throws NotFoundException {
		mySerial = ++User.serial;
		AppiusClaudiusCaecus.blather (newUserLogin, "", "",
				"Instantiated S#" + mySerial + ", populating by login",
				false);
		populateByLogin (newUserLogin);
	}

	/**
	 * @see #acceptAdminMessage(Integer, String)
	 * @param room Ignored
	 * @param string The administrative message
	 * @deprecated use
	 *             {@link #acceptAdminMessage(String, String, String)}
	 *             instead
	 */
	@Deprecated
	public void acceptAdminMessage (final AbstractRoom room,
			final String string) {
		this.acceptAdminMessage ( -1, string);
	}

	/**
	 * Accept a message from an administrator or the system. If the user
	 * is connected, propagate that message to their client.
	 * 
	 * @param room The room in which the administrative message is being
	 *            sent (ignored)
	 * @param string The administrative message.
	 * @deprecated use
	 *             {@link #acceptAdminMessage(String, String, String)}
	 *             instead
	 */
	@Deprecated
	public void acceptAdminMessage (final Integer room,
			final String string) {
		if (null != serverThread) {
			try {
				serverThread.sendAdminMessage (string, true);
			} catch (final UserDeadException e) {
				// Don't ask, don't tell
			}
		}
	}

	/**
	 * Send an administrative or moderator message to the client
	 * 
	 * @param message The body of the message
	 * @param title The non-scrolling title
	 * @param label The label to apply to the corner of the message box
	 */
	public void acceptAdminMessage (final String message,
			final String title, final String label) {
		if (null != serverThread) {
			try {
				serverThread.sendAdminMessage (message, title, label,
						true);
			} catch (final UserDeadException e) {
				// Don't ask, don't care
			}
		}

	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#acceptErrorReply(java.lang.String,
	 *      java.lang.String, org.json.JSONObject,
	 *      org.starhope.appius.game.AbstractRoom)
	 */
	public void acceptErrorReply (final String command,
			final String error, final JSONObject result,
			final AbstractRoom room) {
		final AbstractZone z = getZone ();
		if (z instanceof Zone) {
			final Zone zone = (Zone) z;
			try {
				zone.sendErrorReply (command, error, result, this,
						null == room ? -1 : room.getID ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptGameAction(AbstractUser,
	 *      JSONObject)
	 */
	public void acceptGameAction (final AbstractUser sender,
			final JSONObject action) {
		try {
			serverThread.sendGameActionMessage (sender, action);
		} catch (final UserDeadException e) {
			// nifty, they've died
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptGameStateChange(org.starhope.appius.game.GameEvent,
	 *      org.starhope.appius.game.GameStateFlag)
	 */
	public void acceptGameStateChange (final GameEvent gameCode,
			final GameStateFlag gameState) {
		// No op
	}

	/**
	 * Accept an administrative/moderator message with the full range of
	 * options
	 * 
	 * @param title The title of the message
	 * @param caption The caption; usually ADMIN for administrative
	 *            messages
	 * @param message The message body text
	 */
	public void acceptMessage (final String title,
			final String caption, final String message) {
		if (null != serverThread) {
			try {
				serverThread.sendAdminMessage (message, title, caption,
						true);
			} catch (final UserDeadException e) {
				/* Don't ask, don't tell */
			}
		}
	}

	/**
	 * Accept notification of a user or object joining the room.
	 * 
	 * @param room the room in question
	 * @param object the object leaving the room
	 * @see org.starhope.appius.game.RoomListener#acceptObjectJoinRoom(AbstractRoom,
	 *      RoomListener)
	 */
	public void acceptObjectJoinRoom (final AbstractRoom room,
			final AbstractUser object) {
		if (null != serverThread) {
			try {
				serverThread.sendRoomEnteredByUser (room, object);
				serverThread.sendRoomUserCount (room);
			} catch (final UserDeadException e) {
				// don't ask, don't tell
			}
		}
	}

	/**
	 * @see org.starhope.appius.game.RoomListener#acceptObjectJoinRoom(org.starhope.appius.game.AbstractRoom,
	 *      org.starhope.appius.game.RoomListener)
	 */
	public void acceptObjectJoinRoom (final AbstractRoom room,
			final RoomListener object) {
		if (null != serverThread && object instanceof AbstractUser) {
			try {
				serverThread.sendRoomEnteredByUser (room,
						(AbstractUser) object);
			} catch (final UserDeadException e) {
				/* Don't ask, don't care */
			}
		}
	}

	/**
	 * Accept notification of a user or object departing the room.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptObjectPartRoom(AbstractRoom,
	 *      RoomListener)
	 */
	public void acceptObjectPartRoom (final AbstractRoom room,
			final RoomListener object) {
		final AppiusClaudiusCaecus serv = getServerThread ();
		if (null != serv) {
			try {
				if (object instanceof AbstractUser) {
					serv.sendRoomPartedBy (room, (AbstractUser) object);
				}
				serv.sendRoomUserCount (room);
			} catch (final UserDeadException e) {
				/* No op */
			}
		}
	}

	/**
	 * Accept an out-of-band message from a room.
	 * 
	 * @param sender The user transmitting the out-of-band message
	 * @param body The contents of that message (in JSO)
	 * @param room The room in which the sender is found — and,
	 *            typically, also this recipient.
	 */
	public void acceptOutOfBandMessage (final AbstractUser sender,
			final AbstractRoom room, final JSONObject body) {
		try {
			final JSONObject reply = new JSONObject ();
			reply.put ("body", body);
			reply.put ("sender", sender.getAvatarLabel ());
			if (null != room && null != room.getZone ()) {
				sendSuccessReply ("outOfBand", reply, sender, room
						.getID ());
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * Accept an incoming private message (“whisper”) from another user.
	 * 
	 * @param from The user “whispering” to this one
	 * @param message The contents of the message
	 */
	public void acceptPrivateMessage (final AbstractUser from,
			final String message) {
		try {
			if (null != serverThread) {
				serverThread.sendPrivateMessage (from, message);
			}
		} catch (final UserDeadException e) {
			// nifty, I'm dead… let's ignore that.
			serverThread = null;
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptPublicMessage(org.starhope.appius.user.AbstractUser,
	 *      org.starhope.appius.game.AbstractRoom, java.lang.String)
	 */
	public void acceptPublicMessage (final AbstractUser sender,
			final AbstractRoom room, final String message) {
		this.acceptPublicMessage (sender, message);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptPublicMessage(org.starhope.appius.user.AbstractUser,
	 *      java.lang.String)
	 */
	public void acceptPublicMessage (final AbstractUser from,
			final String message) {
		try {
			if (null != serverThread) {
				serverThread.sendPublicMessage (from, message);
			}
		} catch (final UserDeadException e) {
			// nifty, we died
			serverThread = null;
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @deprecated for shorter
	 *             {@link #acceptSuccessReply(String, JSONObject, AbstractRoom)}
	 * @see org.starhope.appius.user.AbstractUser#acceptSuccessReply(org.starhope.appius.types.AbstractZone,
	 *      org.starhope.appius.game.AbstractRoom, java.lang.String,
	 *      org.json.JSONObject)
	 * 
	 * @deprecated zone is ignored, use
	 *             {@link #acceptSuccessReply(String, JSONObject, AbstractRoom)}
	 */
	@Deprecated
	public void acceptSuccessReply (final AbstractZone zone,
			final AbstractRoom room, final String command,
			final JSONObject jsonData) {
		acceptSuccessReply (command, jsonData, room);
	}

	/**
	 * @param command the command sending the successful reply
	 * @param jsonData additional JSON data
	 * @param room the room in which the success happened
	 */
	public void acceptSuccessReply (final String command,
			final JSONObject jsonData, final AbstractRoom room) {
		final AppiusClaudiusCaecus thread = getServerThread ();
		if (null == thread) {
			System.out
			.println ("Suppressing attempt to send to null client");
		} else {
			try {
				thread.sendSuccessReply (command, jsonData, this,
						(null == room ? -1 : room.getID ()));
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}
	}

	/**
	 * Adds a user to this user's buddy list. Note that this does not
	 * implement the buddy-request functionality at this level, that's
	 * implemented by the client at present.
	 * 
	 * @param newBuddy the user to be the new buddy
	 */
	public void addBuddy (final AbstractUser newBuddy) {
		getBuddyList ().addUser (newBuddy);
	}

	/**
	 * Identical to @link {@link #addDefaultFreeItem(int, boolean)} with
	 * “false” for the second parameter
	 * 
	 * @param id the item ID.
	 */
	public void addDefaultFreeItem (final int id) {
		this.addDefaultFreeItem (id, false);
	}

	/**
	 * Add an item which every user gets for free to the user's
	 * inventory, if it does not already exist. This method is used to
	 * enforce a minimum inventory upon users. Examples of default free
	 * items include the Basic 8 Toots patterns, and the default
	 * TootBook theme.
	 * 
	 * @param id the item ID
	 * @param forceActive if true, force the item to be active upon
	 *            adding it to the user's inventory
	 */
	public void addDefaultFreeItem (final int id,
			final boolean forceActive) {
		if ( !this.hasItem (id)) {
			this.addItem (InventoryItem.getByID (id));
			if (forceActive) {
				blog ("Adding default item " + id
						+ " and setting it as active.");
				for (final InventoryItem i : getInventory ()) {
					if (i.getID () == id) {
						i.setActive (true);
					}
				}
			}
		}
	}

	/**
	 * @param clothingItem The item to be added to the user's inventory
	 *            as a free gift (or prize)
	 */
	protected void addFreeClothing (final ClothingItem clothingItem) {
		this.addDefaultFreeItem (clothingItem.getID ());
	}

	/**
	 * <p>
	 * Create a gift subscription for a user, to last the given number
	 * of months plus the given number of days.
	 * </p>
	 * <p>
	 * To create a lifetime subscription, provide 1000 as the number of
	 * months.
	 * </p>
	 * 
	 * @param months either 1000 (or any greater value over 1000) for a
	 *            lifetime subscription, or, the number of months for a
	 *            gift subscription.
	 * @param days the number of days for a gift subscription. Ignored
	 *            for lifetime subscriptions.
	 */
	@SuppressWarnings ("cast")
	public void addGiftSubscription (final int months, final int days) {
		UserEnrolment subscription = null;
		try {
			if (999 < months) {
				subscription = new UserEnrolment ("life", Enrolment
						.getByID (6).getProductID (), getUserID ());
				subscription.activate (false);
				subscription.flush ();
				return;
			}
			if (0 < months || 0 < days) {
				subscription = new UserEnrolment ("gift", Enrolment
						.getByID (8).getProductID (), getUserID ());
				final Calendar cal = Calendar.getInstance ();
				subscription.activate (false);
				cal.setTimeInMillis (subscription.getBegins ()
						.getTime ());
				cal.add (Calendar.MONTH, ((int) months));
				cal.add (Calendar.DATE, days);
				subscription.setExpires (new java.sql.Date (cal
						.getTimeInMillis ()));
				subscription.flush ();
				return;
			}
		} catch (final NotFoundException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
	}

	/**
	 * Add an item to the user's inventory.
	 * 
	 * @param itemID The item's database ID number.
	 */
	public void addItem (final int itemID) {
		this.addItem (InventoryItem.getByID (itemID));
	}

	/**
	 * Add an item to the user's inventory. This does not conduct a
	 * purchase event.
	 * 
	 * @param item The item to be added to inventory.
	 */
	public void addItem (final InventoryItem item) {
		if (null == item) {
			AppiusClaudiusCaecus
			.reportBug ("Someone tried to add a null item to inventory of user #"
					+ userID);
			return;
		}
		Connection con = null;
		PreparedStatement store = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			store = con
			.prepareStatement (
					"INSERT INTO inventory (userID, itemID, slot, isActive, color, x, y, facing) VALUES (?,?,?,?,?,?,?,?)",
					Statement.RETURN_GENERATED_KEYS);
			store.setInt (1, userID);
			store.setInt (2, item.getID ());
			if (item.getSlotNumber () < 1) {
				store.setNull (3, Types.INTEGER);
			} else {
				store.setInt (3, item.getSlotNumber ());
			}
			store.setString (4, item.isActive () ? "Y" : "N");
			if (item.isWearable ()) {
				final Colour color = item.asClothing ().getColor ();
				if (null == color) {
					store.setNull (5, Types.INTEGER);
				} else {
					store.setInt (5, (int) color.toLong ());
				}
			} else {
				store.setNull (5, Types.INTEGER);
			}
			if (item.isFurniture ()) {
				final HomeDecorItem decor = item.asHomeDecorItem ();
				store.setInt (6, decor.getX ());
				store.setInt (7, decor.getY ());
				store.setString (8, decor.getFacing ());
			} else {
				store.setNull (6, Types.INTEGER);
				store.setNull (7, Types.INTEGER);
				store.setNull (8, Types.VARCHAR);
			}
			store.executeUpdate ();
			blog (" added item #" + item.getID () + " to inventory");
			final ResultSet newSlot = store.getGeneratedKeys ();
			if (newSlot.next ()) {
				item.setSlotHarsh (newSlot.getInt (1));
				blog (" new inventory slot # " + item.getSlotNumber ());
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != store) {
				try {
					store.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		getInventory ().add (item);
	}

	/**
	 * @param roomNumber add a room to the user's house
	 */
	public void addRoom (final int roomNumber) {
		rooms.add (roomNumber);
	}

	/**
	 * Affirm that this is a free (non-premium) member and remove
	 * clothing and patterns.
	 */
	protected void affirmFreeMember () {
		doffClothes ();
		doffPatterns ();
	}

	/**
	 * Ensure that this user has the benefits of being a paid member,
	 * effective immediately
	 */
	public void affirmPaidMember () {
		approvedDate = new Date (System.currentTimeMillis ());
		canEnterChatZone = true;
		canEnterMenuZone = true;
		canTalk = true;
		parentApprovedName = true;
		changed ();
	}

	/**
	 * Approve the user's requested name, and make it active
	 * 
	 * @param abstractUser the user approving the name (requires
	 *            moderator privileges)
	 * @throws PrivilegeRequiredException if the user approving the name
	 *             does not have moderator privileges
	 */
	public void approveName (final AbstractUser abstractUser)
	throws PrivilegeRequiredException {
		abstractUser.assertStaffLevel (User.STAFF_LEVEL_MODERATOR);
		nameApprovedByUserID = abstractUser.getUserID ();
		nameApprovedAt = new Timestamp (System.currentTimeMillis ());
		login = requestedName;
		System.out.println ("Username set to: " + login
				+ "\nRequested set to: " + requestedName);
		changed ();
	}

	/**
	 * Assert that this user must have the given staff level (or
	 * greater). Throws an exception if this is untrue.
	 * 
	 * @param staffLevelNeeded The minimum staff level which is being
	 *            asserted
	 * @throws PrivilegeRequiredException if the minimum staff level is
	 *             not met.
	 */
	public void assertStaffLevel (final int staffLevelNeeded)
	throws PrivilegeRequiredException {
		if (staffLevel < staffLevelNeeded)
			throw new PrivilegeRequiredException (
			"Insufficient level of staff privileges for this operation");
	}

	/**
	 * Attend to an user who may previously have been ignored
	 * 
	 * @param otherGuy the user to whom to now attend
	 */
	public void attend (final AbstractUser otherGuy) {
		getIgnoreList ().removeUser (otherGuy);
	}

	/**
	 * Ban a user, preventing any future access to the server.
	 * 
	 * @param bannedBy The moderator by which the user was banned.
	 * @param bannedReason the reason for which the user was banned
	 * @throws PrivilegeRequiredException if the user does not have
	 *             moderator-level (or better) privileges
	 */
	public void ban (final AbstractUser bannedBy,
			final String bannedReason)
	throws PrivilegeRequiredException {
		if (isOnline () && null != getZone ()) {
			getZone ().tellEaves (
					this,
					getRoom (),
					"ban",
					String.format ("by %s for %s", bannedBy
							.getAvatarLabel (), bannedReason));
		}
		bannedBy.assertStaffLevel (User.STAFF_LEVEL_MODERATOR);
		isBanned = true;
		isActive = false;
		isCanceled = false;
		kickedReasonCode = bannedReason;
		kickedByUserID = bannedBy.getUserID ();
		kickedUntil = new Timestamp (Integer.MAX_VALUE);
		changed ();
		if (null != serverThread) {
			serverThread.sendAdminDisconnect (String.format (
					LibMisc.getText ("banned") + "\n\n"
					+ LibMisc.getText ("rule." + bannedReason),
					getUserName ()), "", (hasStaffLevel (bannedBy
							.getStaffLevel ()) ? bannedBy.getAvatarLabel ()
									: "Lifeguard"), "ban");
		}
	}

	/**
	 * send a message with various debugging information to the journals
	 * 
	 * @param message the message to be recorded
	 */
	public void blog (final String message) {
		if (null == serverThread) {
			AppiusClaudiusCaecus.blather (getAvatarLabel (),
					(getRoom () == null ? "" : "Room “"
						+ getRoom ().getName () + "” in ")
						+ (getZone () == null ? "" : "Zone “"
							+ getZone ().getName () + "”"), "",
							message, false);
		} else {
			serverThread.tattle (message);
		}
	}

	/**
	 * If the user is a teen (13+) or adult, they are allowed to approve
	 * their own account. This is a boolean test for that fact.
	 * 
	 * @return true, if the user is permitted to approve their own
	 *         account (via their own eMail address). False, if they
	 *         require parent approval.
	 */
	public boolean canApproveSelf () {
		return ageGroup == AgeBracket.Teen
		|| ageGroup == AgeBracket.Adult;
	}

	/**
	 * @return true, if the user is permitted to log in to a beta test server
	 */
	public boolean canBetaTest () {
		return canBetaTest || hasStaffLevel (1);
	}

	/**
	 * @param eventID the event ID to be canceled
	 */
	public void cancelEvent (final int eventID) {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("UPDATE events SET completionTimestamp=NOW(), points=0, peanuts=0 WHERE ID=?");
			st.setInt (1, eventID);
			st.execute ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * Determine whether the user can log in with the given password.
	 * 
	 * @param passGuess The user's attempted password
	 * @return true, if the user gave the correct password, and is
	 *         allowed to log in. False, if any of these is not true
	 *         (but does not give any information why)
	 */
	public boolean canLogIn (final String passGuess) {
		return passGuess.equals (password) && isActive && !isKicked ();
	}

	/**
	 * <p>
	 * Only adults are allowed to make purchases. Determine whether this
	 * user is allowed to make purchases, or if we should ask them to
	 * get their parents to buy things for them.
	 * </p>
	 * <p>
	 * In the future, this <em>might not</em> be just a test to check
	 * whether the user is an adult. We might, for example, have kids
	 * with gift cards that will be able to make some purchases on their
	 * own.
	 * </p>
	 * 
	 * @return true, if this user is allowed to purchase things. False,
	 *         if they have to get their parents' permission.
	 */
	public boolean canMakePurchase () {
		return ageGroup == AgeBracket.Adult;
	}

	/**
	 * @return the canTalk
	 */
	public boolean canTalk () {
		return canTalk;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#changed()
	 */
	@Override
	public void changed () {
		blog ("Something has changed.");
		updateCache ();
		flush ();
	}

	/**
	 * @param room the new room to be joined
	 * @return the old room which was departed (if any)
	 */
	public AbstractRoom changeRoom (final AbstractRoom room) {
		serverThread.tattle ("/#/ changeRoom from "
				+ (null == currentRoom ? "null" : currentRoom
						.getMoniker ()) + " to "
						+ (null == room ? "null" : room.getMoniker ()));
		final AbstractRoom oldRoom = currentRoom;
		if (null != room) {
			currentRoom = room;
		}
		return oldRoom;
	}

	/**
	 * @param passwordGuess the supplied password that we want to check
	 * @return true, if the password supplied is correct
	 */
	@Override
	public boolean checkPassword (final String passwordGuess) {
		return getPassword ().equals (passwordGuess);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @param o The other user
	 * @return the two users compared based upon user display names
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo (final User o) {
		return o.getDisplayName ().compareTo (getDisplayName ());
	}

	/**
	 * Mark a message as being deleted in the messages database.
	 * 
	 * @param messageID The message ID to be marked as deleted.
	 * @return true if anything were deleted
	 */
	public boolean deleteMail (final int messageID) {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("UPDATE messages SET isDeleted='D' WHERE ID=?");

			st.setInt (1, messageID);
			return st.executeUpdate () != 0;
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return false;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.HasVariables#deleteVariable(java.lang.String)
	 */
	public void deleteVariable (final String key) {
		userVariables.remove (key);
	}

	/**
	 * Doff a wearable item
	 * 
	 * @see User#wear(ClothingItem)
	 * @param item the item to be removed
	 */
	public void doff (final ClothingItem item) {
		Connection con = null;
		PreparedStatement doff = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			doff = con
			.prepareStatement ("UPDATE inventory SET inventory.isActive='N' WHERE itemID = ? AND inventory.userID = ?");

			doff.setInt (1, item.getID ());
			doff.setInt (2, userID);
			doff.executeUpdate ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != doff) {
				try {
					doff.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
		try {
			fetch_inventory ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * Get naked
	 */
	public void doffClothes () {
		for (final InventoryItem item : this.getItemsByType ("clothes")) {
			blog ("Removing clothing item " + item.toString ());
			doff (item.asClothing ());
		}
	}

	/**
	 * Get bare
	 */
	public void doffPatterns () {
		for (final InventoryItem item : this
				.getItemsByType ("patterns")) {
			blog ("Removing clothing item " + item.toString ());
			doff (item.asClothing ());
		}
	}

	/**
	 * Remove Pivitz
	 */
	public void doffPivitz () {
		for (final InventoryItem item : this.getItemsByType ("pivitz")) {
			doff (item.asClothing ());
		}
	}


	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @param eventID WRITEME
	 * @param itemID WRITEME
	 * @throws NotFoundException WRITEME
	 * @throws NonSufficientFundsException WRITEME
	 * @throws AlreadyExistsException WRITEME
	 */
	public void endEventPurchaseRaw (final int eventID, final int itemID)
	throws NotFoundException, NonSufficientFundsException,
	AlreadyExistsException {
		this.endEventPurchaseRaw (eventID, AppiusConfig
				.getItemCreationTemplate (itemID));
	}

	/**
	 * WRITEME: document this method (brpocock, Dec 30, 2009)
	 * 
	 * @param eventID WRITEME
	 * @param itemCreationTemplate WRITEME
	 * @throws NotFoundException WRITEME
	 * @throws NonSufficientFundsException WRITEME
	 * @throws AlreadyExistsException WRITEME
	 */
	protected abstract void endEventPurchaseRaw (int eventID,
			ItemCreationTemplate itemCreationTemplate)
	throws NotFoundException, NonSufficientFundsException,
	AlreadyExistsException;

	/**
	 * end a multiplayer event … WRITEME details?
	 * 
	 * @param eventID the event instance ID
	 * @param moniker the moniker of the event in progress
	 * @param gameCode the game code of the multiplayer game event
	 * @param score the score of this player
	 * @param scores the set of user ID's and scores of all players
	 * @return the JSON object describing the end of this event
	 * @throws JSONException if the event's results can't be expressed
	 *             in JSON
	 */
	public abstract JSONObject endMultiplayerEvent (final int eventID,
			final String moniker, final String gameCode,
			final BigDecimal score,
			final LinkedHashMap <Integer, Integer> scores)
	throws JSONException;

	/**
	 * This is an overriding method.
	 * 
	 * @throws JSONException WRITEME
	 * @see org.starhope.appius.user.AbstractUser#endMultiplayerEvent(java.lang.Integer,
	 *      java.lang.String, java.lang.String, java.math.BigDecimal,
	 *      java.util.LinkedHashMap)
	 */
	public JSONObject endMultiplayerEvent (final Integer eventID,
			final String gameMoniker, final String gameCode,
			final BigDecimal playerScoreDecimal,
			final LinkedHashMap <Integer, Integer> sortedScores)
	throws JSONException {
		return endMultiplayerEvent ((int) eventID, gameMoniker,
				gameCode, playerScoreDecimal, sortedScores);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals (final Object o) {
		if ( ! (o instanceof User)) return false;
		return this.equals ((User) o);
	}

	/**
	 * Determine if two user objects are identical. Two objects are
	 * equal if they have both the same serial number and user ID.
	 * 
	 * @param o the other user
	 * @return true, if the two instances are totally identical
	 */
	public boolean equals (final User o) {
		return o.getSerial () == getSerial ()
		&& o.getUserID () == getUserID ();
	}

	/**
	 * Equip an item
	 * 
	 * @param item The item to be equipped
	 * @throws GameLogicException if the user does not have the item
	 */
	public void equip (final InventoryItem item)
	throws GameLogicException {
		if ( !inventory.contains (item))
			throw new GameLogicException (
					"Can not equip an item that this user does not own",
					item, null);
		item.setActive (true);
	}

	/**
	 * Fetch the user's avatar information from the database
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_avatarInfo (final ResultSet resultSet)
	throws SQLException {

		avatarClass = new AvatarClass (resultSet.getInt ("avatarClass"));

		baseColor = new Colour (resultSet.getInt ("baseColor"));
		extraColor = new Colour (resultSet.getInt ("extraColor"));

	}

	/**
	 * Fetch the chat colours for this user from the database
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_chatColours (final ResultSet resultSet)
	throws SQLException {
		chatFG = new Colour (resultSet.getInt ("chatFG"));
		chatBG = new Colour (resultSet.getInt ("chatBG"));
	}

	/**
	 * Fetch the user's date of birth and related information from the
	 * database record provided
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_dobInfo (final ResultSet resultSet)
	throws SQLException {
		try {
			birthDate = resultSet.getDate ("birthDate");
		} catch (final SQLException e) {
			birthDate = null;
		}

		final String group = resultSet.getString ("ageGroup");
		if (group.equals ("K")) {
			ageGroup = AgeBracket.Kid;
		} else if (group.equals ("T")) {
			ageGroup = AgeBracket.Teen;
		} else if (group.equals ("A")) {
			ageGroup = AgeBracket.Adult;
		} else if (group.equals ("X")) {
			ageGroup = AgeBracket.System;
		} else {
			ageGroup = AgeBracket.System;
			AppiusClaudiusCaecus
			.reportBug ("Unexpected users.ageGroup value = "
					+ group);
		}

	}

	/**
	 * Sub-fetch routine called by {@link #set(ResultSet)} to handle
	 * inventory load.
	 * 
	 * @throws SQLException if the underlying inventory record(s) is/are
	 *             corrupted in some way
	 * @see #set(ResultSet)
	 */
	private synchronized void fetch_inventory () throws SQLException {
		inventory.clear ();

		Connection con = null;
		PreparedStatement findUserItems = null;
		ResultSet userItems = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			findUserItems = con
			.prepareStatement ("SELECT slot, x, y, facing, color, ID, isActive, itemTypeID AS typeID, userID, name, filename, description, portrait, value FROM inventory LEFT JOIN items ON inventory.itemID=items.ID WHERE userID=?");

			findUserItems.setInt (1, userID);
			if (findUserItems.execute ()) {
				userItems = findUserItems.getResultSet ();
				while (userItems.next ()) {
					final InventoryItem item = InventoryItem
					.getFromInventory (userItems);
					if (null != item) {
						inventory.add (item);
					}
				}
			}
			haveInventory = true;
		} catch (final SQLException e) {
			throw e;
		} finally {
			if (null != userItems) {
				try {
					userItems.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != findUserItems) {
				try {
					findUserItems.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}

	}

	/**
	 * Fetch the user's language and dialect information from the
	 * database record
	 * 
	 * @param resultSet the database record from which to be loaded
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_language (final ResultSet resultSet)
	throws SQLException {
		language = resultSet.getString ("language");
		dialect = resultSet.getString ("dialect");
	}

	/**
	 * Fetch the user's last activity information from the database
	 * record
	 * 
	 * @throws SQLException if the record can't be interpreted.
	 * @see #set(ResultSet)
	 */
	protected void fetch_laston () throws SQLException {
		Connection con = null;
		PreparedStatement laston = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			laston = con
			.prepareStatement ("SELECT * FROM userLaston WHERE userID=? LIMIT 1");
			laston.setInt (1, userID);
			if (laston.execute ()) {
				rs = laston.getResultSet ();
				if (rs.next ()) {
					try {
						lastActive = rs.getTimestamp ("timestamp");
					} catch (final SQLException e) {
						lastActive = null;
					}
					lastZoneName = rs.getString ("lastZoneName");
				}
			}
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != laston) {
				try {
					laston.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}
	}

	/**
	 * Fetch the user's eMail info from the database record
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_mailInfo (final ResultSet resultSet)
	throws SQLException {
		mail = resultSet.getString ("mail");
		if (resultSet.wasNull ()) {
			mail = null;
		}
		mailConfirmed = resultSet.getDate ("mailConfirmed");
		if (resultSet.wasNull ()) {
			mailConfirmed = null;
		}
	}

	/**
	 * Fetch additional results fields from a database record — mostly
	 * intended to be overridden by derived classes
	 * 
	 * @param resultSet WRITEME
	 * @throws SQLException WRITEME
	 */
	protected void fetch_more (final ResultSet resultSet)
	throws SQLException {
		// No op.
	}

	/**
	 * Fetch information about the user's parent (if any).
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_parentInfo (final ResultSet resultSet)
	throws SQLException {
		parentID = resultSet.getInt ("parentID");
		if (resultSet.wasNull ()) {
			parentID = -1;
		}
		approvedDate = resultSet.getDate ("approvedDate");
		if (resultSet.wasNull ()) {
			approvedDate = null;
		}

		emailPlusDate = resultSet.getDate ("emailPlusDate");
		if (resultSet.wasNull ()) {
			emailPlusDate = null;
		}
	}

	/**
	 * Fetch the password reset question and answer from the provided
	 * database record. See {@link #set(ResultSet)}
	 * 
	 * @param resultSet the database record
	 * @throws SQLException If the records can't be found, or are in an
	 *             invalid format of some kind
	 */
	protected void fetch_passwordResetInfo (final ResultSet resultSet)
	throws SQLException {

		forgotPasswordQuestion = resultSet.getString ("passRecoverQ");
		forgotPasswordAnswer = resultSet.getString ("passRecoverA");
		if (resultSet.wasNull ()) {
			forgotPasswordAnswer = null;
		}

	}

	/**
	 * Fetch the permissions-related portion of a user's record out of
	 * the SQL ResultSet. Used while populating the record from the
	 * database.
	 * 
	 * @param resultSet The ResultSet being used to set the user up.
	 * @throws SQLException if the data in the record can't be
	 *             interpreted.
	 * @see #set(ResultSet)
	 */
	protected void fetch_permissionsInfo (final ResultSet resultSet)
	throws SQLException {
		canTalk = resultSet.getString ("canTalk").equals ("Y");
		canEnterChatZone = resultSet.getString ("canEnterChatZone")
		.equals ("Y");
		canEnterMenuZone = resultSet.getString ("canEnterMenuZone")
		.equals ("Y");
		givenName = resultSet.getString ("givenName");
		if (resultSet.wasNull ()) {
			givenName = null;
		}

		try {
			kickedUntil = resultSet.getTimestamp ("kickedUntil");
		} catch (final SQLException e) {
			kickedUntil = null;
		}
		if (resultSet.wasNull ()) {
			kickedUntil = null;
		}
		kickedReasonCode = resultSet.getString ("kickedReasonCode");
		if (resultSet.wasNull ()) {
			kickedReasonCode = null;
		}
		kickedByUserID = resultSet.getInt ("kickedBy");
		if (resultSet.wasNull ()) {
			kickedByUserID = -1;
		}
		isActive = false;
		isCanceled = false;
		isBanned = false;
		final String activity = resultSet.getString ("isActive");
		if ("OK".equals (activity)) {
			isActive = true;
		} else if ("CAN".equals (activity)) {
			isCanceled = true;
		} else if ("BAN".equals (activity)) {
			isBanned = true;
		} else {
			AppiusClaudiusCaecus
			.reportBug ("users.isActive, unacceptable value = "
					+ activity);
		}

		canBetaTest = "Y".equals (resultSet.getString ("canBetaTest"));
	}

	/**
	 * Fetch the user's basic information: user ID, user name, and
	 * password — from the provided database record
	 * 
	 * @param resultSet database record to interpret
	 * @throws SQLException if the record can't be interpreted
	 * @see #set(ResultSet)
	 */
	protected void fetch_userBasicInfo (final ResultSet resultSet)
	throws SQLException {
		userID = resultSet.getInt ("ID");

		login = resultSet.getString ("userName");

		if (resultSet.wasNull ()) {
			login = null;
		}
		password = resultSet.getString ("password");

		try {
			registeredAt = resultSet.getTimestamp ("registeredAt");
		} catch (final java.sql.SQLException e) {
			if (e.getMessage ().contains ("0000-00-00")) {
				registeredAt = null;
			} else throw e;
		}

	}

	/**
	 * @see org.starhope.appius.user.Person#flush()
	 */
	@Override
	public abstract void flush ();

	/**
	 * WRITEME: document this method (brpocock, Jan 8, 2010)
	 * 
	 * @see #flush()
	 * @param update WRITEME
	 * @param bgColumnNumber WRITEME
	 * @param fgColumnNumber WRITEME
	 * @throws SQLException WRITEME
	 */
	protected void flush_breakOut_chatColours (
			final PreparedStatement update, final int bgColumnNumber,
			final int fgColumnNumber) throws SQLException {
		update.setInt (bgColumnNumber, (int) getChatBG ().toLong ());
		update.setInt (fgColumnNumber, (int) getChatFG ().toLong ());
	}

	/**
	 * Breakout of the flush operation to update the user's date of
	 * birth and age group
	 * 
	 * @param update the prepared statement for the update
	 * @throws SQLException if the data can't be stored into the pending
	 *             update
	 */
	protected void flush_breakOut_dob (final PreparedStatement update)
	throws SQLException {
		if (birthDate == null) {
			update.setString (8, "0000-00-00");
		} else {
			update.setDate (8, getBirthDate ());
		}
		update.setString (9, ageGroup == AgeBracket.Kid ? "K"
				: ageGroup == AgeBracket.Teen ? "T"
						: ageGroup == AgeBracket.Adult ? "A" : "X");

	}

	/**
	 * WRITEME: document this method (brpocock, Sep 23, 2009)
	 * 
	 * @see #flush()
	 * @param update WRITEME
	 * @throws SQLException WRITEME
	 */
	protected void flush_breakOut_language (
			final PreparedStatement update) throws SQLException {
		update.setString (10, "en");
		update.setString (11, "US");
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 23, 2009)
	 * 
	 * @see #flush()
	 * @param update WRITEME.
	 * @throws SQLException WRITEME
	 */
	protected void flush_breakOut_loginPass (
			final PreparedStatement update) throws SQLException {

		if (login == null) {
			update.setNull (1, java.sql.Types.VARCHAR);
		} else {
			update.setString (1, login);
		}
		update.setString (2, getPassword ());
		if (givenName == null) {
			update.setNull (18, java.sql.Types.VARCHAR);
		} else {
			update.setString (18, givenName);
		}
		update.setString (23, needsNaming () ? "Y" : "N");

		update.setString (28, forgotPasswordQuestion);
		update.setString (29, forgotPasswordAnswer);

	}

	/**
	 * Flush the user's eMail information to the database
	 * 
	 * @see #flush()
	 * @param update The database PreparedStatement from
	 *            {@link #flush()}
	 * @throws SQLException if the record can't be prepared for update
	 */
	protected void flush_breakOut_mail (final PreparedStatement update)
	throws SQLException {
		if (null == mail) {
			update.setNull (6, Types.VARCHAR);
		} else {
			update.setString (6, mail);
		}
		if (mailConfirmed == null) {
			update.setNull (7, Types.DATE);
		} else {
			update.setDate (7, getMailConfirmed ());
		}
		update.setString (25, canContact ? "Y" : "N");

	}

	/**
	 * Prepare the name-request details for {@link #flush()}
	 * 
	 * @see #flush()
	 * @param update the PreparedStatement to be updated
	 * @throws SQLException if the data can't be set up for some reason
	 */
	protected void flush_breakOut_nameRequest (
			final PreparedStatement update) throws SQLException {
		if (null == requestedName) {
			update.setNull (26, java.sql.Types.VARCHAR);
		} else {
			update.setString (26, requestedName);
		}
		update.setTimestamp (31, nameRequestedAt);
		if (null == requestedName) {
			update.setNull (32, Types.VARCHAR);
		} else {
			update.setString (32, requestedName);
		}

	}

	/**
	 * Prepare the user's parent's details for {@link #flush()}
	 * 
	 * @see #flush()
	 * @param update the PreparedStatement to be updated
	 * @throws SQLException if the data can't be set up for some reason
	 */
	protected void flush_breakOut_parent (final PreparedStatement update)
	throws SQLException {
		if (parentID >= 0) {
			update.setInt (12, getParentID ());
		} else {
			update.setNull (12, java.sql.Types.INTEGER);
		}
		if (approvedDate == null) {
			update.setNull (13, java.sql.Types.DATE);
		} else {
			update.setDate (13, getApprovedDate ());
		}
		if (emailPlusDate == null) {
			update.setNull (14, java.sql.Types.DATE);
		} else {
			update.setDate (14, emailPlusDate);
		}
	}

	/**
	 * set permissions for a database flush
	 * 
	 * @see #flush()
	 * 
	 * @param update the SQL statement being prepared
	 * @throws SQLException if the data can't be prepared for SQL
	 *             injection
	 */
	protected void flush_breakOut_permissions (
			final PreparedStatement update) throws SQLException {
		update.setString (15, canTalk ? "Y" : "N");
		update.setString (16, canEnterChatZone ? "Y" : "N");
		update.setString (17, canEnterMenuZone ? "Y" : "N");

		if (kickedUntil == null || kickedByUserID == -1
				|| kickedReasonCode == null) {
			update.setNull (19, java.sql.Types.DATE);
			update.setNull (20, java.sql.Types.VARCHAR);
			update.setNull (21, java.sql.Types.INTEGER);
		} else {
			update.setTimestamp (19, kickedUntil);
			update.setString (20, kickedReasonCode);
			update.setInt (21, kickedByUserID);
		}
		if (1 != (isActive ? 1 : 0) + (isCanceled ? 1 : 0)
				+ (isBanned ? 1 : 0)) {
			AppiusClaudiusCaecus
			.reportBug ("User is not one of: isActive, isCanceled, isBanned (#"
					+ userID + ")");
		}
		update.setString (22, isActive ? "OK" : isCanceled ? "CAN"
				: "BAN");
		update.setBigDecimal (24, new BigDecimal (staffLevel));
	}

	/**
	 * Prepare the “referer” field for {@link #flush()}
	 * 
	 * @see #flush()
	 * @param update the PreparedStatement for update
	 * @param column the referer column number in the database
	 * @throws SQLException if the SQL can't be set up
	 */
	protected void flush_breakOut_referer (
			final PreparedStatement update, final int column)
	throws SQLException {
		update.setString (column, referer);
	}

	/**
	 * Send the user their forgotten password if they know the answer to
	 * their secret question. WRITEME clarify
	 * 
	 * @param forgottenPasswordQ The question being answered
	 * @param forgottenPasswordA The answer provided
	 * @return true if answer is correct and false if it is not also
	 *         triggers the send password e-mail if correct.
	 */
	@Override
	public boolean forgotPassword (final String forgottenPasswordQ,
			final String forgottenPasswordA) {

		if (null == forgottenPasswordA
				|| null == forgottenPasswordQ
				|| "".equals (forgottenPasswordA)
				|| "".equals (forgottenPasswordQ)
				|| isCanceled
				|| isBanned
				|| !forgotPasswordQuestion.equals (forgottenPasswordQ)
				|| !forgotPasswordAnswer
				.equalsIgnoreCase (forgottenPasswordA))
			return false;

		try {
			remindPassword ();
		} catch (final NotReadyException e) {
			return false;
		}
		return true;
	}

	/**
	 * Generate a new "anonymous user name" for the user.
	 */
	public void generateSystemName () {
		login = "";
		needsNaming = true;
		String tryName = "";
		int tries = 0;
		do {
			tryName = getSystemNameAdjective () + "."
			+ getSystemNameNoun ();
			final Random r = new Random (System.nanoTime ()
					* System.currentTimeMillis ());
			if (tries++ > 10) {
				tryName += String.valueOf (r.nextInt (100));
			}
		} while ( !User.isNameAvailable (tryName));
		login = tryName;
		changed ();
	}

	/**
	 * @return A JSON array of clothing being worn (including pattern
	 *         and Pivitz) in the order preferred by the client
	 */
	public JSONObject getActiveClothing () {
		final JSONObject clothes = new JSONObject ();

		int i = 0;
		for (final String layer : ClothingItem.getClothingLayerNames ()) {
			final Iterator <InventoryItem> items = getInventory ()
			.iterator ();
			while (items.hasNext ()) {
				final InventoryItem item = items.next ();
				if (null == item) {
					AppiusClaudiusCaecus
					.reportBug ("got a null in inventory, for "
							+ userID);
				} else {
					// blog (item.toString ());
					if (item.isWearable ()

							&& item.isActive ()
							&& item.asClothing ().getMountPoint ()
							.equals (layer)) {
						try {
							clothes.put (String.valueOf (i++ ), item
									.toJSON ());
						} catch (final JSONException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
				}
			}
		}
		return clothes;
	}

	/**
	 * get the decorations active (placed) in a room of this user's
	 * house
	 * 
	 * @param roomNumber which room of the user's house
	 * @return home décor items in this room
	 * @throws NotFoundException if the room does not exist
	 */
	public Collection <HomeDecorItem> getActiveDecorations (
			final int roomNumber) throws NotFoundException {
		if ( !rooms.contains (roomNumber)) {
			if (0 == roomNumber || 1 == roomNumber) {
				// free rooms
				addRoom (roomNumber);
			} else throw new NotFoundException (
					"User does not have room #" + roomNumber);
		}

		final LinkedList <HomeDecorItem> decorations = new LinkedList <HomeDecorItem> ();
		for (final InventoryItem item : this
				.getItemsByType (new String [] { "furniture",
				"structure" })) {
			if (item.isActive ()) {
				final HomeDecorItem decor = item.asHomeDecorItem ();
				if (null == decor) {
					// ...
				} else if (decor.getRoom () == roomNumber) {
					decorations.add (decor);
				}
			}
		}
		return decorations;
	}

	/**
	 * Returns the active item for an item type.
	 * 
	 * @param t An Item type to match by.
	 * @return The active inventory item for type t
	 * @see InventoryItem#identifiesAs(String)
	 */
	public InventoryItem getActiveItemByType (final InventoryItemType t) {
		System.out.println ("Getting active items by type for type: "
				+ t.getCanIdentifyAs ());
		return this.getActiveItemByType (t.getCanIdentifyAs ());
	}

	/**
	 * Returns the active item for an item type.
	 * 
	 * @param t A string of the item type. Use Constants from
	 *            {@link InventoryItem}
	 * @return The active inventory item for type t
	 * @see InventoryItem#identifiesAs(String)
	 */
	public InventoryItem getActiveItemByType (final String t) {
		for (final InventoryItem i : this.getItemsByType (t)) {
			if (i.identifiesAs (t) && i.isActive ()) return i;
		}
		return null;
	}

	/**
	 * Get the current age of the user. This is set up such that the
	 * user's age will increment on their birthday.
	 * 
	 * @return The user's legal age, in years.
	 */
	@SuppressWarnings ("deprecation")
	public int getAge () {
		if (null == birthDate) return 1000;
		final java.util.Date now = new java.util.Date ();
		int yearsOld = now.getYear () - birthDate.getYear ();
		now.setYear (birthDate.getYear ());
		if (now.compareTo (birthDate) < 0) {
			--yearsOld; // no birthday yet this year
		}
		return yearsOld;
	}

	/**
	 * @return the ageGroup
	 */
	public AgeBracket getAgeGroup () {
		return ageGroup;
	}

	/**
	 * @see org.starhope.appius.user.Person#getApprovalCookie()
	 */
	@Override
	public abstract String getApprovalCookie ();

	/**
	 * @return the date on which this account was approved
	 */
	public Date getApprovedDate () {
		return approvedDate;
	}

	/**
	 * @see #getApprovedDate()
	 * @return Returns an user-visible string describing whether the
	 *         user has been approved, and if so, when
	 */
	public String getApprovedDateString () {
		if (null == approvedDate) return "Not Approved";
		return "Approved on " + approvedDate.toString ();
	}

	/**
	 * @return the avatarClassID
	 */
	public AvatarClass getAvatarClass () {
		return avatarClass;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#getAvatarLabel()
	 */
	public String getAvatarLabel () {
		final String name = getUserName ();
		if (null == name || "".equals (name))
			return "#" + getUserID ();
		return name;
	}

	/**
	 * @return the baseColor
	 */
	public Colour getBaseColor () {
		return baseColor;
	}

	/**
	 * @return the birthDate
	 */
	public Date getBirthDate () {
		return birthDate;
	}

	/**
	 * Get the user's buddy list (a {@link UserList})
	 * 
	 * @return the user's buddy list
	 */
	public UserList getBuddyList () {
		if (null == buddyList) {
			buddyList = AppiusConfig.newUserList (this, true);
		}
		return buddyList;
	}

	/**
	 * Get the names of everyone on the user's buddy list. See
	 * {@link #getBuddyList()}
	 * 
	 * @return set of user strings
	 */
	public Collection <String> getBuddyListNames () {
		return getBuddyList ().asNames ();
	}

	/**
	 * Get the names of everyone on the user's buddy list.
	 * 
	 * @return String array of user names
	 */
	public String [] getBuddyListNamesAsArray () {
		final Collection <String> buddiesCol = getBuddyList ()
		.asNames ();
		final String [] buddies = new String [buddiesCol.size ()];
		int i = 0;
		for (final String buddy : buddiesCol) {
			buddies [i++ ] = buddy;
		}

		return buddies;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return String.valueOf (userID);
	}

	/**
	 * @return the chat background colour
	 */
	public Colour getChatBG () {
		return chatBG;
	}

	/**
	 * @return the chat foreground colour
	 */
	public Colour getChatFG () {
		return chatFG;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#getConfirmationTemplate()
	 */
	@Override
	public String getConfirmationTemplate () {
		return AgeBracket.Kid == ageGroup ? "ParentNotification"
				: "UserConfirmation";
	}

	/**
	 * Get the IP address or hostname from which the user is connected
	 * (if we can tell). See {@link #isOnline()} for the usual caveats
	 * 
	 * @return Hostname "/" IP address. Hostname is often absent.
	 */
	public String getConnectedFrom () {
		if (isOnline ()) return getIpAddress ();
		return "";
	}

	/**
	 * @return the dialect
	 */
	@Override
	public String getDialect () {
		return dialect;
	}

	/**
	 * @see org.starhope.appius.user.Person#getDisplayName()
	 */
	@Override
	public String getDisplayName () {
		if (null != givenName) return givenName;
		if (null != login) return login;
		if (null != requestedName) return requestedName;
		if (null != mail) return "<" + mail + ">";
		return "(Unnamed)";
	}

	/**
	 * @return the emailPlusDate
	 */
	public Date getEmailPlusDate () {
		return emailPlusDate;
	}

	/**
	 * @see User#getEmailPlusDate()
	 * @return Returns a user-displayable English string describing when
	 *         the eMail Plus confirmation happened, if it has.
	 */
	public String getEmailPlusDateString () {
		if (null == emailPlusDate) return "No eMail Plus";
		return "eMail Plus on " + emailPlusDate.toString ();
	}

	/**
	 * @return the extraColor
	 */
	public Colour getExtraColor () {
		return extraColor;
	}

	/**
	 * @return the facing
	 */
	public String getFacing () {
		return facing;
	}

	/**
	 * @param slotNumber WRITEME
	 * @return the home décor item in the given inventory slot
	 * @throws NotFoundException WRITEME
	 */
	public HomeDecorItem getFurnitureBySlot (final int slotNumber)
	throws NotFoundException {
		final Iterator <InventoryItem> inv = getInventory ()
		.iterator ();
		while (inv.hasNext ()) {
			final InventoryItem item = inv.next ();
			if (item.isFurniture ()) {
				final HomeDecorItem furn = item.asHomeDecorItem ();
				if (slotNumber == furn.getSlotNumber ()) return furn;
			}
		}
		blog (" No furniture in slot " + slotNumber
				+ " is found in inventory");
		throw new NotFoundException (String.valueOf (slotNumber));
	}

	/**
	 * If the user has a game item equipped (e.g. a key), then get that
	 * item. Otherwise, returns null.
	 * 
	 * @return the game item currently being carried / currently
	 *         equipped, or null if no item is equipped.
	 */
	private GameEquipItem getGameEquipItem () {
		// TODO Auto-generated method stub (brpocock, Dec 1, 2009)
		return null;
	}

	/**
	 * Return the status of the indicated game flag. Note that all game
	 * flags default to false.
	 * 
	 * @param name The unique identifier for the game flag
	 * @return the state (or presence) of the flag
	 */
	public boolean getGameFlag (final String name) {
		return false;
	}

	/**
	 * @return the given name of the user, if one were set.
	 */
	@Override
	public String getGivenName () {
		return givenName;
	}

	/**
	 * Returns the historical contents of this user's record.
	 * 
	 * @param after If non-null, specifies the date after which we want
	 *            to view records. To see all records, back to the
	 *            creation of the user record, supply a null.
	 * @param limit If this is a positive number, it limits the results
	 *            to this number of records.
	 * @return A map of timestamps to key/value pairs. All values are
	 *         expressed as strings (even though they may have numeric,
	 *         enumerative, or date / datetime types in the database),
	 *         since this is primarily (only?) for human-viewable
	 *         auditing.
	 */
	@Override
	public HashMap <Timestamp, HashMap <String, String>> getHistory (
			final Date after, final int limit) {
		final HashMap <Timestamp, HashMap <String, String>> history = new HashMap <Timestamp, HashMap <String, String>> ();
		String limits = "";
		if (null != after) {
			limits += " AND stamp > ? ";
		}
		limits += " ORDER BY stamp DESC ";
		if (limit > 0) {
			limits += " LIMIT " + limit;
		}
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM userHistory WHERE ID=? "
					+ limits);

			st.setInt (1, userID);
			if (null != after) {
				st.setDate (2, after);
			}
			st.execute ();
			rs = st.getResultSet ();
			while (rs.next ()) {
				Timestamp when;
				try {
					when = rs.getTimestamp ("stamp");
				} catch (final SQLException e) {
					when = null;
				}
				final HashMap <String, String> details = new HashMap <String, String> ();
				details.put ("userName", rs.getString ("userName"));
				details.put ("password", rs.getString ("password"));
				details.put ("avatarClass", ""
						+ rs.getInt ("avatarClass"));
				details.put ("baseColor", "" + rs.getInt ("baseColor"));
				details.put ("extraColor", ""
						+ rs.getInt ("extraColor"));
				details.put ("mail", rs.getString ("mail"));
				details.put ("birthDate", rs.getDate ("birthDate")
						.toString ());
				details.put ("ageGroup", rs.getString ("ageGroup"));
				details.put ("language", rs.getString ("language"));
				details.put ("dialect", rs.getString ("dialect"));
				details.put ("parentID", "" + rs.getInt ("parentID"));
				details.put ("approvedDate", rs
						.getDate ("approvedDate").toString ());
				details.put ("emailPlusDate", rs.getDate (
				"emailPlusDate").toString ());
				details.put ("canTalk", rs.getString ("canTalk"));
				details.put ("canEnterChatZone", rs
						.getString ("canEnterChatZone"));
				details.put ("canEnterMenuZone", rs
						.getString ("canEnterMenuZone"));
				details.put ("givenName", rs.getString ("givenName"));
				try {
					final Timestamp ku = rs
					.getTimestamp ("kickedUntil");
					details.put ("kickedUntil", ku.toString ());
				} catch (final SQLException e) {
					details.put ("kickedUntil", "null");
				}
				details.put ("kickedReasonCode", rs
						.getString ("kickedReasonCode"));
				details.put ("kickedBy", "" + rs.getInt ("kickedBy"));
				details.put ("isActive", rs.getString ("isActive"));
				details.put ("needsNaming", rs
						.getString ("needsNaming"));
				details.put ("staffLevel", ""
						+ rs.getInt ("staffLevel"));
				details.put ("peanuts", rs.getBigDecimal ("peanuts")
						.toPlainString ());
				details.put ("tootTimeLeft", rs.getBigDecimal (
				"tootTimeLeft").toPlainString ());
				details.put ("tootTimeLeftMinutes", rs.getBigDecimal (
				"tootTimeLeftMinutes").toPlainString ());
				details.put ("tootTimeRefill", rs.getBigDecimal (
				"tootTimeLeftRefill").toPlainString ());
				details.put ("tootTimerType", rs
						.getString ("tootTimerType"));
				details.put ("canContact", rs.getString ("canContact"));
				history.put (when, details);
			}
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return history;
	}

	/**
	 * @return the other users being ignored by this one
	 */
	public UserList getIgnoreList () {
		if (null == ignoreList) {
			ignoreList = AppiusConfig.newUserList (this, false);
		}
		return ignoreList;
	}

	/**
	 * @return the inventory
	 */
	protected synchronized HashSet <InventoryItem> getInventory () {
		if (haveInventory) return inventory;

		try {
			fetch_inventory ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a SQLException in getInventory", e);
		}
		return inventory;
	}

	/**
	 * @return The string form of the user's IP address (may be IPv4 or
	 *         IPv6)
	 * @deprecated Smart Fox Server misspelling of
	 *             {@link #getIPAddress()}
	 */
	@Deprecated
	public String getIpAddress () {
		return getIPAddress ();
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#getIPAddress()
	 */
	public String getIPAddress () {
		if (null != getServerThread ())
			return serverThread.getIPAddress ();
		return "";
	}

	/**
	 * Get all items that identify as the type string. See
	 * {@link InventoryItem#identifiesAs(String)} for the canonical
	 * list.
	 * 
	 * @param typeString A type string specified in
	 *            {@link InventoryItem#identifiesAs(String)}
	 * @see InventoryItem#identifiesAs(String)
	 * @return an array of inventory items that identify as the given
	 *         type
	 */
	public Collection <InventoryItem> getItemsByType (
			final String typeString) {
		return this.getItemsByType (new String [] { typeString });
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 28, 2009)
	 * 
	 * @param types The set of types of inventory items which are wanted
	 * @see InventoryItem#identifiesAs(String)
	 * @return get items in the
	 */
	public Collection <InventoryItem> getItemsByType (
			final String [] types) {

		for (final String type : types) {
			if (InventoryItem.PATTERNS.equals (type)) {
				this.addDefaultFreeItem (808); // Zap
				this.addDefaultFreeItem (813); // Flora
				this.addDefaultFreeItem (814); // Cupid
				this.addDefaultFreeItem (809); // Dottie
				this.addDefaultFreeItem (812); // Moo
				this.addDefaultFreeItem (811); // Sparkle
				this.addDefaultFreeItem (810); // Superstar
				this.addDefaultFreeItem (815); // Li'l MC
			}
			if (InventoryItem.MUSIC.equals (type)) {
				this.addDefaultFreeItem (1072);
				this.addDefaultFreeItem (1073);
				this.addDefaultFreeItem (1074);
				this.addDefaultFreeItem (1075);
			}
			if (InventoryItem.TB_AVATAR_BG.equals (type)) {
				this
				.addDefaultFreeItem (TootBookAvatarBG.DEFAULT_THEME_ITEM_NUMBER);
			}
			if (InventoryItem.TB_BOX_STYLES.equals (type)) {
				this
				.addDefaultFreeItem (TootBookBoxStyle.DEFAULT_THEME_ITEM_NUMBER);
			}
			if (InventoryItem.TB_ICON.equals (type)) {
				this
				.addDefaultFreeItem (TootBookIcon.DEFAULT_THEME_ITEM_NUMBER);
			}
			if (InventoryItem.TB_PAGE_BG.equals (type)) {
				this
				.addDefaultFreeItem (TootBookPageBG.DEFAULT_THEME_ITEM_NUMBER);
			}
			if (InventoryItem.TB_TITLE_BG.equals (type)) {
				this
				.addDefaultFreeItem (TootBookTitleBG.DEFAULT_THEME_ITEM_NUMBER);
			}
			if (InventoryItem.STATIONERY.equals (type)) {
				this
				.addDefaultFreeItem (Stationery.DEFAULT_FREE_STATIONERY);
			}
		}

		final LinkedList <InventoryItem> results = new LinkedList <InventoryItem> ();
		for (final InventoryItem i : getInventory ()) {
			for (final String type : types) {
				if (i.identifiesAs (type)) {
					results.add (i);
				}
			}
		}

		return results;
	}

	/**
	 * Get all items that identify as the type string. See
	 * {@link InventoryItem#identifiesAs(String)} for the canonical
	 * list.
	 * 
	 * @param typeString A type string specified in
	 *            {@link InventoryItem#identifiesAs(String)}
	 * @see InventoryItem#identifiesAs(String)
	 * @return an array of inventory items that identify as the given
	 *         type
	 */
	public InventoryItem [] getItemsByTypeAsArray (
			final String typeString) {
		final Collection <InventoryItem> itemsByType = this
		.getItemsByType (new String [] { typeString });
		return itemsByType.toArray (new InventoryItem [itemsByType
		                                               .size ()]);
	}

	/**
	 * @return the user ID by which this user was kicked (or banned)
	 */
	public int getKickedByUserID () {
		return kickedByUserID;
	}

	/**
	 * @return the message explaining the user being kicked (or banned)
	 */
	public String getKickedMessage () {
		String s = "";
		if ( !isKicked () && !isBanned ()) {
			s = LibMisc.getText ("kick.not-kicked", getLanguage (),
					getDialect ());
		} else {
			s = LibMisc
			.getText (isBanned () ? "banned" : "kick.kicked")
			+ "\n\n"
			+ LibMisc
			.getText ("rule." + getKickedReasonCode ());
		}
		return String.format (s, getUserName ());
	}

	/**
	 * @return the kickedReasonCode
	 */
	public String getKickedReasonCode () {
		// default getter (brpocock, Jul 8, 2009)
		return kickedReasonCode;
	}

	/**
	 * @return the kickedUntil
	 */
	public Timestamp getKickedUntil () {
		// default getter (brpocock, Jul 8, 2009)
		return kickedUntil;
	}

	/**
	 * @return the time at which the user last was logged on or active
	 */
	public Timestamp getLastActive () {
		return lastActive;
	}

	/**
	 * @return the last (or current) zone for the player
	 */
	public String getLastZone () {
		if (isOnline ()) {
			final AbstractZone currentZone = getZone ();
			if (null != currentZone) {
				final String zoneName = currentZone.getName ();
				if (zoneName.charAt (0) != '$') {
					lastZoneName = zoneName;
				}
			}
		}
		return lastZoneName;
	}

	/**
	 * Get the user's current location (room and zone) if the user is
	 * online; or, “null” if the user is not online.
	 * 
	 * @return room and zone
	 */
	public RoomAndZone getLocation () {
		final String roomName = null != currentRoom ? currentRoom
				.getMoniker () : "";
				if ("".equals (roomName) && null != currentRoom) {
					if (null == currentRoom.getMoniker ()) {
						AppiusClaudiusCaecus
						.reportBug ("Non-null room gives null moniker");
					}
					if (0 == currentRoom.getMoniker ().length ()) {
						AppiusClaudiusCaecus
						.reportBug ("Room has null moniker");
					}
				}
				String zoneName = null == serverThread ? "" : serverThread
						.getZone ().getName ();
				if (zoneName.length () > 0 && zoneName.charAt (0) == '$') {
					zoneName = "";
				}
				return new RoomAndZone (roomName, zoneName);
	}

	/**
	 * @return the user's login
	 */
	public String getLogin () {
		return login;
	}

	/**
	 * @return the mySerial
	 */
	public int getMySerial () {
		// default getter (brpocock, Oct 30, 2009)
		return mySerial;
	}

	/**
	 * @return Get the user's login name
	 */
	public String getName () {
		return getUserName ();
	}

	/**
	 * @return The time at which the user's name was approved, if it has
	 *         been (null if not).
	 */
	public Timestamp getNameApprovedAt () {
		return nameApprovedAt;
	}

	/**
	 * @return the user ID of the moderator who approved this user's
	 *         name, if approved. (-1 if not)
	 */
	public long getNameApprovedByUserID () {
		return nameApprovedByUserID;
	}

	/**
	 * @return the date & time at which the moderator approved this
	 *         user's name, if approved (null if not).
	 */
	public Timestamp getNameRequestedAt () {
		return nameRequestedAt;
	}

	/**
	 * @return this user's parent (if any) — null if none.
	 */
	public Parent getParent () {
		final long parent = getParentID ();
		if (parent >= 0) return Parent.getByID (getParentID ());
		return null;
	}

	/**
	 * @return true, if the parent has approved this user's name
	 */
	public boolean getParentApprovedName () {
		return parentApprovedName;
	}

	/**
	 * @return the ID number of this user's parent, if any (-1 if none)
	 */
	public int getParentID () {
		return parentID;
	}

	/**
	 * @return a JSON object representing the user's Passport.
	 * @throws SQLException if the passport records can't be obtained or
	 *             interpreted
	 * @throws JSONException if the passport can't be represented in
	 *             JSON form
	 */
	public JSONObject getPassport_JSON ()
	throws SQLException, JSONException {
		final JSONObject passport = new JSONObject ();
		final JSONObject ret = new JSONObject ();

		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT moniker FROM userWorlds LEFT JOIN roomList ON worldID=roomList.ID WHERE userID=?");
			st.setInt (1, userID);
			int i = 0;
			if (st.execute ()) {
				rs = st.getResultSet ();
				while (rs.next ()) {
					passport.put (String.valueOf (i++ ), rs
							.getString ("moniker"));
				}
			}
			ret.put ("passport", passport);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
		return ret;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#getPotentialUserName()
	 */
	@Override
	public String getPotentialUserName () {
		return getUserNameOrRequest ();
	}

	/**
	 * @return JSONObject 'self' including userName and avatarClass
	 */
	public JSONObject getPublicInfo () {
		final JSONObject userInfo = new JSONObject ();
		try {
			userInfo.put ("avatar", avatarClass.getFilename ());
			userInfo.put ("avatarClass", avatarClass.getID ());
			userInfo.put ("chatFG", chatFG.toLong ());
			userInfo.put ("chatBG", chatBG.toLong ());
			local_publicInfo (userInfo);
			userInfo.put ("avatarClass_B", avatarClass
					.getDefaultBaseColor ().toLong ());
			userInfo.put ("avatarClass_E", avatarClass
					.getDefaultExtraColor ().toLong ());
			userInfo.put ("avatarClass_P", avatarClass
					.getDefaultPatternColor ().toLong ());
			final AbstractRoom room = getRoom ();
			if (null != room) {
				userInfo.put ("inRoom", room.getMoniker ());
			}
			userInfo.put ("userName", getAvatarLabel ());
			final JSONObject colors = new JSONObject ();
			if (avatarClass.canColor ()) {
				colors.put ("0", baseColor.toLong ());
				colors.put ("1", extraColor.toLong ());
			}
			userInfo.put ("colors", colors);
			userInfo.put ("clothes", getActiveClothing ());
			final GameEquipItem gameItem = getGameEquipItem ();
			userInfo.put ("gameItem", null == gameItem ? "" : gameItem
					.getID ());
			userInfo.put ("vars", getVariablesJSON ());
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		return userInfo;
	}

	/**
	 * Get the referral/affiliate code from this user's initial signup
	 * 
	 * @return the referral or affiliate code
	 */
	public String getReferer () {
		return referer;
	}

	/**
	 * @return the registeredAt
	 */
	public Timestamp getRegisteredAt () {
		return registeredAt;
	}

	/**
	 * @return the date on which this user first registered
	 * @see #registeredAt
	 */
	public java.util.Date getRegisteredDate () {
		return new java.util.Date (registeredAt.getTime ());
	}

	/**
	 * @see #getRegisteredDate()
	 * @return Returns an user-visible string describing when the user
	 *         was registered
	 */
	public String getRegisteredDateString () {
		if (null == registeredAt) return "Registration date unknown";
		return "Registered on " + registeredAt.toString ();
	}

	/**
	 * @return the name that the user requested
	 */
	public String getRequestedName () {
		return null == requestedName ? login : ""
			.equals (requestedName) ? login : requestedName;
	}

	/**
	 * <p>
	 * Get the eMail address of a responsible person: either the player,
	 * or the parent.
	 * </p>
	 * <p>
	 * Currently, kids 13-17 return their own mail.
	 * </p>
	 * 
	 * @return the eMail address
	 */
	@Override
	public String getResponsibleMail () {
		if (ageGroup == AgeBracket.Kid) {
			final Parent p = getParent ();
			if (null == p) return null;
			return p.getMail ();
		}
		return getMail ();
	}

	/**
	 * @return The room in which the user currently exists. This might
	 *         potentially be null.
	 */
	public AbstractRoom getRoom () {
		return currentRoom;
	}

	/**
	 * @return the number of the user's current room; or -1 if the user
	 *         isn't in any room
	 */
	public int getRoomNumber () {
		if (null == currentRoom) return -1;
		return currentRoom.getID ();
	}

	/**
	 * Get the unique instance serial number for this instance of this
	 * user. Ideally, there should never be two instances of the same
	 * user in core at once; these serial numbers are used for debugging
	 * to be able to detect if that were to happen.
	 * 
	 * @return the unique instance serial number
	 */
	public long getSerial () {
		return User.serial;
	}

	/**
	 * Get the Appius Claudius Caecus server thread (if any) associated
	 * with this User (if they are logged-in).
	 * 
	 * @return the active server thread, or null
	 */
	public synchronized AppiusClaudiusCaecus getServerThread () {
		return serverThread;
	}

	/**
	 * <p>
	 * If we can see the SmartFox Server, and if this user has a peer
	 * "User" signed in to that server now, then get that peer user
	 * object so that we can do things to them in real time (in the
	 * game).
	 * </p>
	 * <p>
	 * If running with a native Appius server (raw on the port), this is
	 * unnecessary.
	 * </p>
	 * 
	 * @return The SmartFox Server's peer User object (if any), or null
	 *         if we can't obtain it (if we can't see the SFS or if the
	 *         user isn't logged in right now)
	 */
	// private it.gotoandplay.smartfoxserver.data.User getSFSPeer () {
	// if (null != userName && !"".equals (userName)
	// && User.canSeeSmartFoxServer ()) {
	// for (final Object o : AppiusClaudiusCaecus.getAllZones ().toArray
	// ()) {
	// if (o instanceof Zone) {
	// final Zone z = (Zone) o;
	// final it.gotoandplay.smartfoxserver.data.User sfsUser = z
	// .getUserByName (userName);
	// if (null != sfsUser)
	// return sfsUser;
	// } else {
	// AppiusClaudiusCaecus.reportBug ("This is not a Zone: " + o);
	// }
	// }
	// }
	// return null;
	// }
	/**
	 * @return the staffLevel
	 */
	public int getStaffLevel () {
		// default getter (brpocock, Jul 8, 2009)
		return staffLevel;
	}

	/**
	 * @return an adjective usable as part of a system-generated name
	 */
	private String getSystemNameAdjective () {
		final String [] adjectives = {};
		return adjectives [(int) (Math.random () * adjectives.length)];
	}

	/**
	 * @return a noun usable as part of a system-generated name
	 */
	private String getSystemNameNoun () {
		final String [] nouns = {};
		return nouns [(int) (Math.random () * nouns.length)];
	}

	/**
	 * @return the targetX
	 */
	public double getTargetX () {
		return targetX;
	}

	/**
	 * @return the targetY
	 */
	public double getTargetY () {
		return targetY;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#getTravelRate()
	 */
	public double getTravelRate () {
		return travelRate;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#getTravelStart()
	 */
	public long getTravelStart () {
		return travelStartTime;
	}

	/**
	 * @return all enrolments for this user
	 */
	public Collection <UserEnrolment> getUserEnrolments () {
		return UserEnrolment.getAllForUserID (userID);
	}

	/**
	 * @return all enrolments for this user
	 */
	public UserEnrolment [] getUserEnrolmentsAsArray () {
		final Collection <UserEnrolment> allForUserID = UserEnrolment
		.getAllForUserID (userID);
		return allForUserID.toArray (new UserEnrolment [allForUserID
		                                                .size ()]);
	}

	/**
	 * @return the userID
	 */
	public int getUserID () {
		return userID;
	}

	/**
	 * @return the user's login
	 */
	public String getUserName () {
		return login;
	}

	/**
	 * return the user's login name, if they have one; or the name that
	 * they have requested, if it hasn't been approved yet. If they have
	 * not requested any name at all, returns the string "(No name)" .
	 * 
	 * @return the user's login name, or their requested name, or a
	 *         string indicating that they have neither.
	 */
	public String getUserNameOrRequest () {
		if (null != login) return login;
		if (null != requestedName) return requestedName;
		setNeedsNaming (true);
		return "(No name)";
	}

	/**
	 * Get all user variables in a hash map
	 * 
	 * @return A hashmap containing all user variables
	 */
	public HashMap <String, String> getUserVariables () {
		final HashMap <String, String> ret = new HashMap <String, String> ();
		ret.putAll (userVariables);
		// ret.put ("d", getVariable ("d"));
		return ret;
	}

	/**
	 * @param string WRITEME
	 * @return WRITEME
	 */
	public String getVariable (final String string) {
		// if (string.equals ("d") ) {
		// final StringBuilder location = new StringBuilder ();
		// location.append ((int) x);
		// location.append ('~');
		// location.append ((int) y);
		// location.append ('~');
		// location.append ((int) targetX);
		// location.append ('~');
		// location.append ((int) targetY);
		// location.append ('~');
		// location.append (facing);
		// return location.toString ();
		// }
		return userVariables.get (string);
	}

	/**
	 * Get all values in JSON form
	 *
	 * @return the user variables in JSON form
	 */
	public JSONObject getVariablesJSON () {
		final JSONObject vars = new JSONObject ();
		for (final Entry<String,String>var : getUserVariables ().entrySet ()) {
			try {
				vars.put (var.getKey (), var.getValue ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a JSONException in getVariablesJSON",
						e);
			}
		}
		return vars;
	}

	/**
	 * @return the x
	 */
	public double getX () {
		return x;
	}

	/**
	 * @return the y
	 */
	public double getY () {
		return y;
	}

	/**
	 * Get the user's current Zone, if the user is online.
	 * 
	 * @return the current zone of the user, if any. Null if the user is
	 *         offline.
	 */
	public AbstractZone getZone () {
		return null == serverThread ? null : serverThread.getZone ();
	}

	/**
	 * Get a (hopefully unique) hash code for this user.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc
		.makeHashCode ( (String.valueOf (userID) + '/' + User.serial));
	}

	/**
	 * Determine whether this user has this item
	 * 
	 * @param id The item ID
	 * @return true, if the user has the item
	 */
	public boolean hasItem (final int id) {
		final Iterator <InventoryItem> i = getInventory ().iterator ();
		while (i.hasNext ()) {
			final InventoryItem item = i.next ();
			if (item.getID () == id) return true;
		}
		return false;
	}

	/**
	 * determine whether the user has an item
	 * 
	 * @param item the item in question
	 * @return true, if the user has the given item; else, false
	 */
	public boolean hasItem (final InventoryItem item) {
		return this.hasItem (item.getID ());
	}

	/**
	 * Returns true if the user has the asserted staff level, or a staff
	 * level which includes it. Returns false, otherwise.
	 * 
	 * @param staffLevelNeeded The minimum staff level for which we are
	 *            testing.
	 * @return True, if the user meets the minimum staff level stated;
	 *         false, otherwise.
	 */
	public boolean hasStaffLevel (final int staffLevelNeeded) {
		try {
			assertStaffLevel (staffLevelNeeded);
		} catch (final PrivilegeRequiredException e) {
			return false;
		}
		return true;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#hasVariable(java.lang.String)
	 */
	public boolean hasVariable (final String string) {
		return userVariables.containsKey (string);
	}

	/**
	 * Calling this method indicates that this user wants to ignore the
	 * other specified user.
	 * 
	 * @param otherUser The user, whom this user wishes to ignore
	 */
	public void ignore (final AbstractUser otherUser) {
		getIgnoreList ().addUser (otherUser);
	}

	/**
	 * WRITEME: document this method (brpocock, Jan 4, 2010)
	 * 
	 * @param birthDate1 WRITEME
	 */
	protected abstract void insertIntoDatabase (final Date birthDate1);

	/**
	 * Invalidate the live cache records of this user after s/he has
	 * signed off.
	 */
	public void invalidateCache () {
		System.out.println (" Removing user #" + userID + " “" + login
				+ "” from user liveCache");
		User.liveCache.remove ("uid=" + userID);
		User.liveCache.remove ("log="
				+ login.toLowerCase (Locale.ENGLISH));
		if (null != currentRoom && null != currentRoom.getZone ()) {
			serverThread
			.tattle ("/#/ dropping user from Zone containing "
					+ currentRoom.getMoniker ());
			currentRoom.getZone ().remove (this);
		}
		User.dumpLiveCache ();
	}

	/**
	 * @return true, if this is an active user account (able to sign in,
	 *         potentially)
	 */
	public boolean isActive () {
		return isActive;
	}

	/**
	 * See {@link #approvedDate} for discussion
	 * 
	 * @return true, if this account has been approved, and is still
	 *         active
	 */
	public boolean isApproved () {
		return isActive && null != approvedDate;
	}

	/**
	 * @return true, if the account is banned (neither active nor
	 *         canceled)
	 */
	public boolean isBanned () {
		// default getter (brpocock, Jul 8, 2009)
		return isBanned;
	}

	/**
	 * @return true, if today is the user's birthday
	 */
	@SuppressWarnings ("deprecation")
	public boolean isBirthday () {
		if (AgeBracket.System == ageGroup) return true;
		final Date now = new Date (System.currentTimeMillis ());
		if (birthDate.getMonth () == now.getMonth ()
				&& birthDate.getDay () == now.getDay ()) return true;
		if (birthDate.getMonth () == 2 && now.getMonth () == 2
				&& birthDate.getDay () == 29 && now.getDay () == 28)
			return true;
		return false;
	}

	/**
	 * @return true if the user account was canceled (not active, nor
	 *         banned)
	 */
	public boolean isCanceled () {
		return isCanceled;
	}

	/**
	 * @return the canContact
	 */
	@Override
	public boolean isCanContact () {
		// default getter (brpocock, Jul 8, 2009)
		return canContact;
	}

	/**
	 * @return the canEnterChatZone
	 */
	public boolean isCanEnterChatZone () {
		// default getter (brpocock, Jul 8, 2009)
		return canEnterChatZone;
	}

	/**
	 * @return the canEnterMenuZone
	 */
	public boolean isCanEnterMenuZone () {
		// default getter (brpocock, Jul 8, 2009)
		return canEnterMenuZone;
	}

	/**
	 * 
	 * @return {@link #canTalk()}
	 */
	public boolean isCanTalk () {
		return canTalk ();
	}

	/**
	 * Returns true if the user has been kicked offline (and the time
	 * has not yet elapsed). Returns false otherwise. This does
	 * <em>not</em> check the status as to whether the user might have
	 * been banned.
	 * 
	 * @return true, if the user is kicked offline.
	 */
	public boolean isKicked () {
		if (null == kickedUntil) return false;
		if (kickedUntil.compareTo (new Timestamp (System
				.currentTimeMillis ())) <= 0) {
			kickedUntil = null;
			kickedByUserID = 1;
			kickedReasonCode = null;
			return false;
		}
		return true;
	}

	/**
	 * Returns whether this account has a system-provided (not
	 * user-provided) name, or no name at all, and we need to prompt the
	 * user (or parent) to name it.
	 * 
	 * @return true if this user needs to be named
	 */
	public boolean isNeedsNaming () {
		return needsNaming ();
	}

	/**
	 * @return true if there is a flag of some kind on this user, that
	 *         the parent needs to pay attention to.
	 */
	public boolean isNeedsParentAttention () {
		if (getAgeGroup () != AgeBracket.Kid) return false;
		if ( !isApproved () && !isCanceled && !isBanned) return true;
		if (isBanned ()) return true;
		if (isKicked ()) return true;
		if (isNeedsNaming ()) return true;
		if (needsParent ()) return true;
		if (isBanned ()) return true;
		return false;
	}

	/**
	 * Determine whether this user is a player-character, or
	 * non-player-character.
	 * 
	 * @return true, if this User is NPC-controlled.
	 */
	public boolean isNPC () {
		return AgeBracket.System == getAgeGroup ();
	}

	/**
	 * @return true, if the user is online; false, if they're offline
	 *         (or we can't tell for certain, e.g. we can't see the
	 *         SmartFox Server / Appius Claudius Caecus)
	 */
	public boolean isOnline () {
		return serverThread != null;
	}

	/**
	 * @return true, if the user is a paid member (or staff member)
	 */
	public boolean isPaidMember () {
		if (staffLevel > User.STAFF_LEVEL_PUBLIC) return true;

		final Collection <UserEnrolment> enrolments = getUserEnrolments ();
		if (enrolments.size () > 0) {
			for (final UserEnrolment enrolment : enrolments) {
				if (enrolment.isActive ()) return true;
			}
		}
		if (AppiusConfig
				.getConfigBoolOrFalse ("com.tootsville.hedgeWaddleHack")) {
			if (login.equalsIgnoreCase ("waddle458")) {
				blog ("HACK: waddle458 is a paid member");
				return true;
			}
			if (login.equalsIgnoreCase ("hedge99")) {
				blog ("HACK: hedge99 is a paid member");
				return true;
			}
		}
		return false;
	}

	/**
	 * Kick a user offline for a number of minutes.
	 * 
	 * @param kickedBy The user who is kicking this user offline
	 * @param kickedReason The reason for which s/he is being kicked
	 * @param kickedMinutes The duration for which s/he should be
	 *            kicked, in minutes. Read that again: minutes — not
	 *            msec!
	 * @throws PrivilegeRequiredException if the person trying to kick
	 *             this user off doesn't have moderator privileges
	 */
	public void kick (final AbstractUser kickedBy,
			final String kickedReason, final long kickedMinutes)
	throws PrivilegeRequiredException {
		final Timestamp until = new Timestamp (System
				.currentTimeMillis ()
				+ kickedMinutes * 60 * 1000);
		this.kick (kickedBy, kickedReason, until);
	}


	/**
	 * Kick the user offline, until a certain date & time.
	 * 
	 * @param kickedBy The user who is kicking this user offline
	 * @param kickedReason The reason for which s/he is being kicked
	 * @param allowBack The time at which this user is permitted to be
	 *            online again.
	 * @throws PrivilegeRequiredException if the person trying to kick
	 *             this user off doesn't have moderator privileges
	 */
	public synchronized void kick (final AbstractUser kickedBy,
			final String kickedReason, final Timestamp allowBack)
	throws PrivilegeRequiredException {
		final Timestamp welcomeBack = new Timestamp (allowBack
				.getTime ());
		final long kickMaxTime = System.currentTimeMillis ()
		+ kickedBy.getStaffLevel () * 1000L * 60 * 15;
		if (welcomeBack.getTime () > kickMaxTime) {
			kickedBy
			.acceptAdminMessage (
					"You can kick an user for up to "
					+ kickedBy.getStaffLevel ()
					* 15
					+ " minutes. Above that, contact a systems programmer for assistance.",
					"Maximum Kick Time Exceeded", "God");
			welcomeBack.setTime (kickMaxTime);
		}
		kickedBy.assertStaffLevel (User.STAFF_LEVEL_MODERATOR);
		kickedByUserID = kickedBy.getUserID ();
		kickedReasonCode = kickedReason;
		kickedUntil = welcomeBack;
		changed ();
		if (null != serverThread) {
			serverThread.sendAdminDisconnect (String.format (
					LibMisc.getText ("kick.kicked") + "\n\n"
					+ LibMisc.getText ("rule." + kickedReason),
					getUserName ()), "", (hasStaffLevel (kickedBy
							.getStaffLevel ()) ? kickedBy.getAvatarLabel ()
									: "Lifeguard"), "kick");
			serverThread.tattle ("Kicked! By "
					+ kickedBy.getAvatarLabel () + " #"
					+ kickedByUserID + " for " + kickedReason
					+ " until " + welcomeBack.toString ());
		}
		if (isOnline () && null != getZone ()) {
			getZone ().tellEaves (
					this,
					getRoom (),
					"kick",
					String.format ("by %s until %s for %s ", kickedBy
							.getAvatarLabel (),
							welcomeBack.toString (), kickedReason));
		}
	}

	/**
	 * Lift the ban upon this user.
	 * @see org.starhope.appius.user.AbstractUser#liftBan(org.starhope.appius.user.AbstractUser)
	 */
	public void liftBan (final AbstractUser authority)
	throws PrivilegeRequiredException {
		assertStaffLevel (User.STAFF_LEVEL_MODERATOR);
		kickedUntil = new Timestamp (System.currentTimeMillis ());
		setActive (true);
	}

	/**
	 * Hook for special stuff to be done right after a new user account
	 * is created
	 */
	public abstract void local_create ();

	/**
	 * Append information in the subclass to the already-prepared JSON
	 * data for the getPublicInfo call
	 * 
	 * @param userInfo the public info JSON object to which additional
	 *            info. should be appended
	 */
	protected void local_publicInfo (final JSONObject userInfo) {
		/* No op in parent class */
	}

	/**
	 * @param zoneName The name of the zone too which the user has
	 *            logged in
	 * @param newServerThread The server thread in which the user is
	 *            logged in
	 */
	public void loggedIn (final String zoneName,
			final AppiusClaudiusCaecus newServerThread) {
		if (zoneName.charAt (0) != '$') {
			lastZoneName = zoneName;
		}
		setLastActive ();
		setServerThread (newServerThread);
	}

	/**
	 * Record that the user has logged in
	 * 
	 * @param zoneName The zone name into which the user signed in
	 * @param channel The I/O channel on which the user is connected
	 */
	@Deprecated
	public void loggedIn (final String zoneName,
			final SocketChannel channel) {
		lastZoneName = zoneName;
		setLastActive ();
		changed ();
	}

	/**
	 * Validate the user's login attempt, returning a failure message if
	 * it could not happen (e.g. the user is kicked). Returns null if
	 * the user CAN log in. DOES NOT log the user in to SFS though!
	 * 
	 * @param chapSeed The CHAP random seed (or null, if no seed was
	 *            used)
	 * @param passwordGuess The guessed password. This should be an
	 *            MD5sum if CHAP is being used (if the seed is not
	 *            null), or the plaintext password otherwise
	 * @return an error string for the user, or null, if successful.
	 */
	public String login (final String chapSeed,
			final String passwordGuess) {
		if (isCanceled)
			return LibMisc
			.getText ("login.canceled", language, dialect);
		if (isKicked ())
			return LibMisc.getText ("kicked", language, dialect);
		if (isBanned ())
			return LibMisc.getText ("banned", language, dialect);
		boolean passwordOK = false;
		if (null == chapSeed) {
			passwordOK = password.equals (passwordGuess);
		} else {
			// final MD5 md5 = MD5.instance ();
			// passwordOK = md5.getHash (password + chapSeed).equals (
			// passwordGuess);
		}
		if (true == passwordOK) return null;
		return LibMisc.getText ("login.fail", language, dialect);
	}

	/**
	 * Returns true if this user has requested a name but it hasn't yet
	 * been approved by a Lifeguard
	 * 
	 * @return true, if user has requested a new name and it hasn't yet
	 *         been approved
	 */
	public boolean nameNeedsApproval () {
		return null == nameApprovedAt && null != nameRequestedAt;
	}

	/**
	 * @return true if the name hasn't been approved by the parent yet
	 */
	public boolean nameNeedsParentalApproval () {
		return AgeBracket.Kid == ageGroup && false == needsNaming
		&& false == parentApprovedName;
	}

	/**
	 * Returns whether this account has a system-provided (not
	 * user-provided) name, or no name at all, and we need to prompt the
	 * user (or parent) to name it.
	 * 
	 * @return true if this user needs to be named
	 */
	public boolean needsNaming () {
		return needsNaming = null == login || null == requestedName;
	}

	/**
	 * Kid accounts (under 13) require parental confirmation. In order
	 * to get that, we have to get a parental contact. If this field is
	 * false, then the user is either a teenager or adult, or they have
	 * a parent on file. It does <em>not</em> mean that they have had
	 * their account approved: only that they have given us the parental
	 * information (if we needed it). If we ever encounter a user for
	 * whom this flag is true, ask them “who's your daddy?”
	 * 
	 * @return true, if this is a kid account without a known parent
	 *         (yet)
	 */
	public boolean needsParent () {
		return ageGroup == AgeBracket.Kid && parentID == -1;
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#notifyFurnitureInventory(org.starhope.appius.game.AbstractRoom)
	 */
	public void notifyFurnitureInventory (final AbstractRoom room) {
		final JSONObject blah = new JSONObject ();
		try {
			blah.put ("type", "furniture");
			Commands.do_getInventoryByType (blah, this, room);
			blah.put ("type", "structure");
			Commands.do_getInventoryByType (blah, this, room);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * Call this method when the parent determines whether to approve or
	 * disapprove this account.
	 * 
	 * @param whether the parent has approved the account; true =
	 *            approved; false = disapproved
	 */
	public void parentApprovedAccount (final boolean whether) {
		if (isBanned) return;
		if (whether) {
			approvedDate = new Date (System.currentTimeMillis ());
			canEnterChatZone = true;
			canEnterMenuZone = true;
			canTalk = true;
			isActive = true;
			isCanceled = false;
		} else {
			approvedDate = null;
			canEnterChatZone = false;
			canEnterMenuZone = false;
			canTalk = false;
			canContact = false;
			isActive = false;
			isCanceled = true;
		}
		changed ();
	}

	/**
	 * @param whether True if the parent has approved the name; false if
	 *            they disapprove and want a system suggested name.
	 */
	public void parentApprovedName (final boolean whether) {
		if (whether) {
			parentApprovedName = true;
		} else {
			generateSystemName ();
			parentApprovedName = false;
		}
		changed ();
	}

	/**
	 * Private method used to look up User in the database and fill in
	 * the attributes
	 * 
	 * @param id The database ID to load
	 * @throws NotFoundException if the ID isn't found in the database.
	 */
	protected void populateByID (final int id) throws NotFoundException {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM users WHERE ID=?");
			st.setInt (1, id);
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) {
					this.set (rs);
				} else throw new NotFoundException (
						"Can't find any user with ID=" + id);

			} else throw new NotFoundException (
					"Can't find any user with ID=" + id);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * Fetch a user record out of the database based upon its login name
	 * 
	 * @param loadLogin The login name (user name)
	 * @throws NotFoundException if there is no user named that *right
	 *             now*. (There may have been one in the past. Login
	 *             names are maleable.)
	 */
	protected void populateByLogin (final String loadLogin)
	throws NotFoundException {
		Connection con = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("SELECT * FROM users WHERE userName=?");

			st.setString (1, loadLogin);
		} catch (final SQLException e) {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e1) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e1) { /* No Op */
				}
			}
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
		try {
			if (st.execute ()) {
				rs = st.getResultSet ();
				if (rs.next ()) {
					this.set (rs);
				} else throw new NotFoundException (
						"Can't find any user with login name="
						+ loadLogin);
			} else throw new NotFoundException (
					"Can't find any user with login name=" + loadLogin);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} finally {
			if (null != rs) {
				try {
					rs.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			try {
				st.close ();
			} catch (final SQLException e) { /* No Op */
			}
			try {
				con.close ();
			} catch (final SQLException e) { /* No Op */
			}
		}
	}

	/**
	 * WRITEME: document this method (brpocock, Jan 8, 2010)
	 */
	public void postLoginGlobal () {
		final String motd = AppiusClaudiusCaecus.getMOTD ();
		if (motd.length () > 0) {
			acceptAdminMessage (motd, "Message of the Day", "Catvlle");
		}
	}

	/**
	 * XXX Java serialization stuff
	 * 
	 * @param in WRITEME
	 * @throws IOException WRITEME
	 * @throws ClassNotFoundException WRITEME
	 */
	public void readExternal (final ObjectInput in)
	throws IOException, ClassNotFoundException {
		// XXX Auto-generated method stub (brpocock, Dec 9, 2009)

	}

	/**
	 * This is an overriding method. If this user is a staff member,
	 * resets their password.
	 * 
	 * @throws NotReadyException if the reminder can't be sent because
	 *         the user has no confirmed mail address on file
	 * @see org.starhope.appius.user.Person#remindPassword()
	 */
	@Override
	protected void remindPassword () throws NotReadyException {
		if (hasStaffLevel (1)) {
			generateNewPassword ();
		}
		super.remindPassword ();
	}

	/**
	 * Don't want for him to be my buddy any more
	 * 
	 * @param otherGuy who?
	 * @see org.starhope.appius.user.AbstractUser#removeBuddy(org.starhope.appius.user.AbstractUser)
	 */
	public void removeBuddy (final AbstractUser otherGuy) {
		getBuddyList ().removeUser (otherGuy);
	}

	/**
	 * <p>
	 * Rename the user account, updating all necessary related records.
	 * Note, in particular, that Smartfox is wholly dependant upon user
	 * names, so all records related to Smartfox must be updated!
	 * </p>
	 * <p>
	 * If the user is currently online, this will fuck up hilariously, I
	 * think.
	 * </p>
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.Person#rename(java.lang.String)
	 */
	@Override
	public void rename (final String newName) {
		// if (isBanned && User.canSeeSmartFoxServer ()) {
		// if (!"".equals (newName)) {
		// // User.sfs.addBannedUser (new BannedUser (newName,
		// // ""));
		// }
		// if (! (getUserName () == null)
		// && !getUserName ().equals ("")) {
		// // User.sfs.removeBannedUser (new BannedUser (
		// // getUserName (), ""));
		// // UserListsManager.globalRenameUser (getUserName (),
		// // newName);
		// }
		// }
		if (isOnline ())
			throw AppiusClaudiusCaecus
			.fatalBug ("Renaming an account that is currently online is unimplemented");
		login = newName;
		changed ();
	}

	/**
	 * WRITEME: document this method (brpocock, Aug 31, 2009)
	 * 
	 * @param u WRITEME
	 */
	public void reportedToModeratorBy (final AbstractUser u) {
		if (hasStaffLevel (u.getStaffLevel ())) {
			acceptAdminMessage (
					"You have been reported to the lifeguard by "
					+ u.getAvatarLabel (), "Reported!",
			"Lifeguard");
		}
		AppiusClaudiusCaecus.reportBug (" User " + u.getUserID ()
				+ " (" + u.getAvatarLabel () + ") reported user "
				+ userID + " (" + login + ")");
	}

	/**
	 * <p>
	 * Post a request to the lifeguards to get approval of a user name
	 * </p>
	 * <em>Does not work, is not used </em>
	 * 
	 * @param userNameRequested The user name which the user has
	 *            requested
	 * @throws ForbiddenUserException if the user name is forbidden
	 *             (e.g. obscene or previously denied for any reason)
	 * @throws AlreadyUsedException if someone has already requested or
	 *             used it
	 */
	public void requestNewUserName (final String userNameRequested)
	throws AlreadyUsedException, ForbiddenUserException {
		User.assertUserNameAvailable (userNameRequested);
		requestedName = userNameRequested;
		nameRequestedAt = new Timestamp (System.currentTimeMillis ());
		try {
			approveName (User.getByID (1));
		} catch (final PrivilegeRequiredException e) {
			AppiusClaudiusCaecus.reportBug (
					"God can't auto-approve user " + userNameRequested
					+ " name?", e);
		}
		/*
		 * final MonitorEvent ev = new MonitorEvent (
		 * "User name requested", "mb.name"); ev.attachObject
		 * ("User requested name", String.class, requestedName);
		 * ev.attachObject ("User requesting name", User.class, this);
		 * ev.post ();
		 */
		changed ();
	}

	/**
	 * request parent approval
	 */
	public void requestParentApproval () {
		if (null != approvedDate) return;
		if (parentID >= 0) {
			getParent ().requestApproval (this);
		}
	}

	/**
	 * Send a confirmation eMail for a premium user.
	 */
	public void sendConfirmationForPremium () {
		try {
			Mail.sendPremiumMail (this);
		} catch (final FileNotFoundException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final IOException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final DataException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final NamingException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#sendEarnings(org.starhope.appius.game.AbstractRoom,
	 *      java.lang.String)
	 */
	public void sendEarnings (final AbstractRoom room,
			final String string) {
		final JSONObject result = new JSONObject ();
		try {
			result.put ("msg", string);
			result.put ("status", true);
			result.put ("from", "earning");
		} catch (final JSONException e1) {
			// Default catch action, report bug (brpocock, Jan 18, 2010)
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in sendEarnings", e1);
		}
		final AppiusClaudiusCaecus thread = getServerThread ();
		if (null != thread) {
			try {
				thread.sendResponse (result, room.getID (), this);
			} catch (final UserDeadException e) {
				// don't care
			}
		}
	}

	/**
	 * Send a game action to another user.
	 * 
	 * @param from The user sending the game action (to this user)
	 * @param data The game action data
	 * @throws JSONException if the data can't be sent in JSON
	 */
	@Deprecated
	public void sendGameAction (final User from, final JSONObject data)
	throws JSONException {
		acceptGameAction (from, data);
	}

	/**
	 * Send a notification to the user that s/he should reconnect to a
	 * different zone
	 * 
	 * @param refugeeZone the zone to which the user should migrate
	 * @throws UserDeadException WRITEME
	 */
	public void sendMigrate (final AbstractZone refugeeZone)
	throws UserDeadException {
		try {
			final JSONObject migration = new JSONObject ();
			migration.put ("from", "migrate");
			migration.put ("status", "true");
			migration.put ("toZone", refugeeZone.getName ());
			migration.put ("host", refugeeZone.getHost ());
			migration.put ("port", refugeeZone.getPort ());
			serverThread.sendResponse (migration, getRoomNumber ());
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#sendOops()
	 */
	public void sendOops () {
		acceptPrivateMessage (this, "/00p$");
	}

	/**
	 * Deprecated. See the more accurately-named
	 * {@link #acceptPrivateMessage(AbstractUser, String)}
	 * 
	 * @param from the user speaking
	 * @param message the words spoken
	 * @see #acceptPrivateMessage(AbstractUser, String)
	 */
	@Deprecated
	public void sendPrivateMessage (final AbstractUser from,
			final String message) {
		acceptPrivateMessage (from, message);
	}

	/**
	 * Accept a public message.
	 * 
	 * @param from The user speaking
	 * @param speech the words spoken
	 * @see #acceptPublicMessage(AbstractUser, String)
	 * @deprecated use
	 *             {@link #acceptPublicMessage(AbstractUser, String)}
	 */
	@Deprecated
	public void sendPublicMessage (final User from, final String speech) {
		this.acceptPublicMessage (from, speech);
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#sendResponse(org.json.JSONObject)
	 */
	public void sendResponse (final JSONObject result) {
		if (null != serverThread) {
			try {
				serverThread.sendResponse (result, getRoomNumber (),
						true);
			} catch (final UserDeadException e) {
				// No worries
			}
		}
	}

	/**
	 * Send a response to this user's client, if one is connected.
	 * 
	 * @param result the response datagram in JSON
	 * @param room the room from which the response is coming (or -1 for
	 *        not-specified) (ignored)
	 * @deprecated use {@link #sendResponse(JSONObject)}
	 */
	@Deprecated
	public void sendResponse (final JSONObject result, final int room) {
		sendResponse (result);
	}

	/**
	 * Send a “success” reply to the client if one is connected
	 * 
	 * @param source The method returning success
	 * @param reply additional details of the success to be returned to
	 *            the client
	 * @param sender the user reporting success
	 * @param room the room in which the success happened, or -1 if not
	 *            specified
	 * 
	 */
	private void sendSuccessReply (final String source,
			final JSONObject reply, final AbstractUser sender,
			final int room) {
		if (null != serverThread) {
			try {
				serverThread.sendSuccessReply (source, reply, sender,
						room);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a JSONException in sendSuccessReply",
						e);
			}
		}
	}

	/**
	 * Sends the user an asynchronous notification of their user lists'
	 * status. This is normally triggered by a change in the status of
	 * one of the users on these lists. (Note, User Lists are the buddy
	 * list and ignore list.)
	 * 
	 * @see org.starhope.appius.user.AbstractUser#sendUserLists()
	 */
	public void sendUserLists () {
		final JSONObject lists = new JSONObject ();
		try {
			lists
			.put ("buddyList", getBuddyList ().toJSON (
					getZone ()));
			lists.put ("ignoreList", getIgnoreList ().toJSON (
					getZone ()));
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in sendUserLists", e);
		}
		acceptSuccessReply ("getUserLists", lists, getRoom ());
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#sendWardrobe()
	 */
	public void sendWardrobe () {
		final JSONObject wardrobe = getPublicInfo ();
		final JSONObject results = new JSONObject ();
		try {
			results.put ("avatar", wardrobe);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in sendWardrobe", e);
			return;
		}
		final AbstractRoom room = getRoom ();
		for (final AbstractUser viewer : room.getAllUsers ()) {
			if (viewer instanceof User && ((User) viewer).isOnline ()) {
				viewer.acceptSuccessReply ("wardrobe", results, room);
			}
		}
		sendSuccessReply ("wardrobe", results, this, getRoomNumber ());
	}

	/**
	 * @see org.starhope.appius.sql.SQLPeerDatum#set(java.sql.ResultSet)
	 */
	@Override
	protected void set (final ResultSet resultSet) throws SQLException {
		fetch_userBasicInfo (resultSet);
		fetch_avatarInfo (resultSet);
		fetch_mailInfo (resultSet);
		fetch_dobInfo (resultSet);
		fetch_language (resultSet);
		fetch_parentInfo (resultSet);
		fetch_passwordResetInfo (resultSet);
		fetch_permissionsInfo (resultSet);
		fetch_chatColours (resultSet);

		referer = resultSet.getString ("referer");
		if (resultSet.wasNull ()) {
			referer = null;
		}
		setSubclassValues (resultSet);

		needsNaming = resultSet.getString ("needsNaming").equals ("Y");
		staffLevel = resultSet.getInt ("staffLevel");
		canContact = resultSet.getString ("canContact").equals ("Y");

		try {
			nameRequestedAt = resultSet
			.getTimestamp ("nameRequestedAt");
		} catch (final SQLException e) {
			nameRequestedAt = null;
		}
		if (resultSet.wasNull ()) {
			nameRequestedAt = null;
		}
		requestedName = resultSet.getString ("requestedName");
		if (resultSet.wasNull ()) {
			requestedName = null;
		}

		fetch_laston ();

		updateCache ();

		if (isPaidMember ()) {
			affirmPaidMember ();
		} else if (getAvatarClass ().getID () > 8) {
			affirmFreeMember ();
		}
	}

	/**
	 * @param isActive1 the isActive to set
	 */
	public void setActive (final boolean isActive1) {
		isActive = isActive1;
		if (isActive1 == true) {
			isCanceled = false;
			isBanned = false;
		}
		changed ();
	}

	/**
	 * Sets the age group based upon the user's date of birth.
	 * 
	 * @return The age group computed for this user
	 */
	public AgeBracket setAgeGroup () {
		if (ageGroup == AgeBracket.System) return AgeBracket.System;

		final int age = getAge ();

		ageGroup = age >= 18 ? AgeBracket.Adult
				: age >= 13 ? AgeBracket.Teen : AgeBracket.Kid;
				return ageGroup;
	}

	/**
	 * Declares this to be an inhuman, ergo ageless, user account.
	 */
	public void setAgeGroupToSystem () {
		ageGroup = AgeBracket.System;
		birthDate = null;
		changed ();
	}

	/**
	 * @param date the approvedDate to set
	 */
	public void setApprovedDate (final Date date) {
		approvedDate = date;
		changed ();
	}

	/**
	 * @param avatarClass1 the avatarClass to set
	 */
	public void setAvatarClass (final AvatarClass avatarClass1) {
		avatarClass = avatarClass1;
		changed ();
	}

	/**
	 * @param newBaseColor the baseColor to set
	 */
	public void setBaseColor (final Colour newBaseColor) {
		baseColor = newBaseColor;
		changed ();
	}

	/**
	 * @param birthDate1 the birthDate to set
	 */
	public void setBirthDate (final Date birthDate1) {
		birthDate = birthDate1;
		setAgeGroup ();
		if (ageGroup == AgeBracket.Teen || ageGroup == AgeBracket.Adult) {
			if (null == approvedDate) {
				approvedDate = new Date (System.currentTimeMillis ());
			}
			canTalk = true;
			canEnterChatZone = true;
			// canEnterMenuZone = true;
		}
		changed ();
	}

	/**
	 * @param isCanceled1 the isCanceled to set
	 */
	public void setCanceled (final boolean isCanceled1) {
		isCanceled = isCanceled1;
		changed ();
	}

	/**
	 * @param canContact1 the canContact to set
	 */
	@Override
	public void setCanContact (final boolean canContact1) {
		canContact = canContact1;
		changed ();
	}

	/**
	 * @param canEnterChatZone1 the canEnterChatZone to set
	 */
	public void setCanEnterChatZone (final boolean canEnterChatZone1) {
		// default setter (brpocock, Jul 8, 2009)
		canEnterChatZone = canEnterChatZone1;
		changed ();
	}

	/**
	 * @param canEnterMenuZone1 the canEnterMenuZone to set
	 */
	public void setCanEnterMenuZone (final boolean canEnterMenuZone1) {
		canEnterMenuZone = canEnterMenuZone1;
		changed ();
	}

	/**
	 * @param canTalk1 the canTalk to set
	 */
	public void setCanTalk (final boolean canTalk1) {
		canTalk = canTalk1;
		changed ();
	}

	/**
	 * @param newChatBG the new chat background colour
	 */
	public void setChatBG (final Colour newChatBG) {
		chatBG = newChatBG;
		changed ();
	}

	/**
	 * @param newChatFG the chat foreground colour to set
	 */
	public void setChatFG (final Colour newChatFG) {
		chatFG = newChatFG;
		changed ();
	}

	/**
	 * @param emailPlusDate1 the emailPlusDate to set
	 */
	public void setEmailPlusDate (final Date emailPlusDate1) {
		emailPlusDate = emailPlusDate1;
		changed ();
	}

	/**
	 * @param extraColor1 the extraColor to set
	 */
	public void setExtraColor (final Colour extraColor1) {
		extraColor = extraColor1;
		changed ();
	}

	/**
	 * @param newFacing the facing to set
	 */
	public void setFacing (final String newFacing) {
		facing = newFacing;
	}

	/**
	 * @param givenName1 the givenName to set
	 */
	@Override
	public void setGivenName (final String givenName1) {
		givenName = givenName1;
		changed ();
	}

	/**
	 * 
	 */
	public void setLastActive () {
		lastActive = new Timestamp (System.currentTimeMillis ());
	}

	/**
	 * Notify the multiverse that the user is logging in Set up the User
	 * records, indicating that the user has (in fact) logged in to the
	 * game. Record the IP address and Zone server.
	 * 
	 * @param ipAddress The IP address (or something like it) from which
	 *            the user has logged-in
	 * @param zone The active server zone into which the User has
	 *            logged-in.
	 */
	public void setLogin (final String ipAddress, final String zone) {
		Connection con = null;
		PreparedStatement st = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("INSERT INTO userLaston (userID, timestamp, ipAddress, lastZoneName) VALUES (?,NOW(),?,?)");

			st.setInt (1, userID);
			st.setString (2, ipAddress);
			st.setString (3, zone);
			st.execute ();
		} catch (final SQLException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != st) {
				try {
					st.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != con) {
				try {
					con.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}

		}
	}

	/**
	 * @param newMail the mail to set
	 * @throws GameLogicException if attempting to set an eMail address
	 *         on a kid account
	 */
	@Override
	public void setMail (final String newMail)
	throws GameLogicException {
		if (ageGroup == AgeBracket.Kid)
			throw new GameLogicException ("kid mail", newMail, newMail);
		blog ("Setting mail to " + newMail);
		mail = newMail;
		mailConfirmed = null;
		blog ("Mail changed");
		changed ();
		sendConfirmationMail ();
	}

	/**
	 * Sets the user capabilities to allow talking, and permits the user
	 * entry into both chat zones and menu-chat-only zones.
	 * 
	 * @param mailConfirmed1 the date and time at which the user's mail
	 *            was confirmed.
	 */
	@Override
	public void setMailConfirmed (final Date mailConfirmed1) {
		mailConfirmed = mailConfirmed1;
		canTalk = true;
		canEnterChatZone = true;
		canEnterMenuZone = true;
		changed ();
	}

	/**
	 * @param needsNaming1 the needsNaming to set
	 */
	public void setNeedsNaming (final boolean needsNaming1) {
		needsNaming = needsNaming1;
		requestedName = null;
		changed ();
	}

	/**
	 * Set this to be a child account with the specified parent.
	 * 
	 * @param parent the parent to set
	 * @throws GameLogicException if this user is an adult
	 * @throws ForbiddenUserException WRITEME
	 */
	public void setParent (final Parent parent)
	throws GameLogicException, ForbiddenUserException {
		if (null == parent) return;
		if (getAgeGroup () == AgeBracket.Adult)
			throw new GameLogicException (
					"We don't need to know about the parents of adults.",
					this, parent);
		if (parentID != parent.getID ()) {

			if (parent.hasBannedKids ())
				throw new ForbiddenUserException (login);

			if (null == approvedDate
					&& AgeBracket.Kid == getAgeGroup ()) {
				parentID = parent.getID ();

				parent.sendNotificationForChild (this);
			} else {
				parentID = parent.getID ();
			}
			changed ();
		}
		// no change if it's the same parent.
	}

	/**
	 * Set this to be a child account with the specified parent.
	 * 
	 * @param parent the parent to set
	 * @throws GameLogicException if this user is an adult
	 */
	public void setParentByParent (final Parent parent)
	throws GameLogicException {
		if (null == parent) return;
		if (getAgeGroup () == AgeBracket.Adult)
			throw new GameLogicException (
					"We don't need to know about the parents of adults.",
					this, parent);
		if (parentID != parent.getID ()) {
			if (null == approvedDate
					&& AgeBracket.Kid == getAgeGroup ()) {
				parentID = parent.getID ();
			}
			approvedDate = new Date (System.currentTimeMillis ());
			parentApprovedName = true;
			canEnterChatZone = true;
			canEnterMenuZone = true;
			canTalk = true;
			canContact = false;
			parentID = parent.getID ();
			changed ();
		}
		// no change if it's the same parent.
	}

	/**
	 * WRITEME: document this method (brpocock, Sep 22, 2009)
	 * 
	 * @param theReferer WRITEME
	 */
	public void setReferer (final String theReferer) {
		if (null == referer) {
			if ("mimo".equals (theReferer)) {
				final Random rnd = new Random ();
				final int itemNumber = 1108 + rnd.nextInt (3);
				switch (itemNumber) {
				case 1108:
					blog ("A new Mimo Toot gets a Mimo Pivitz.");
					break;
				case 1109:
					blog ("A new Mimo Toot gets a disco Pivitz.");
					break;
				case 1110:
					blog ("A new Mimo Toot gets a popcorn Pivitz.");
					break;
				default:
					blog ("A new Mimo Toot fails to get an expected Pivitz");
				}
				this.addDefaultFreeItem (itemNumber);
				final InventoryItem mimoPivitz = InventoryItem
				.getByID (itemNumber);
				this.wear (mimoPivitz.asClothing ());
			}
			referer = theReferer;
			changed ();
		}
	}

	/**
	 * @param newRegisteredAt the registeredAt to set
	 */
	public void setRegisteredAt (final Timestamp newRegisteredAt) {
		registeredAt = newRegisteredAt;
	}

	/**
	 * Set the user's current room to the given room. This will part
	 * from the prior room, if the user was in a room already. It also
	 * sets the user variable “d” to (-100,-100) coördinates.
	 * 
	 * @param room the room in which the user must exist
	 * @return the room number set
	 */
	public int setRoom (final AbstractRoom room) {
		serverThread.tattle ("/#/ setRoom from "
				+ (null == currentRoom ? "null" : currentRoom
						.getMoniker ()) + " to "
						+ (null == room ? "null" : room.getMoniker ()));

		if (null == room) {
			AppiusClaudiusCaecus
			.reportBug ("I don't want to go to null! Only bad Toots go to null!");
			return null == currentRoom ? -1 : currentRoom.getID ();
		}

		if (null != currentRoom
				&& !currentRoom.getMoniker ().equals (
						room.getMoniker ())) {
			currentRoom.part (this);
			this.setVariable ("d", "-100~-100~-100~-100~"
					+ getFacing () + "~"
					+ (System.currentTimeMillis () - 300));
		}
		currentRoom = room;
		return currentRoom.getID ();
	}

	/**
	 * Set the server thread controlling this user. Disconnect any
	 * pre-existing server thread.
	 * 
	 * @param newThread The new server thread.
	 */
	public synchronized void setServerThread (
			final AppiusClaudiusCaecus newThread) {
		if (null != serverThread) {
			if (serverThread.equals (newThread)) return;
			serverThread.disconnectDuplicate ();
		}
		serverThread = newThread;
		changed ();
	}

	/**
	 * @param newStaffLevel the staffLevel to set
	 */
	public void setStaffLevel (final int newStaffLevel) {
		staffLevel = newStaffLevel;
		changed ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#setStartT(long)
	 */
	public void setStartT (final long when) {
		travelStartTime = when;
	}

	/**
	 * Activate one structural element in lieu of any others that occupy
	 * the same slot.
	 * 
	 * @param item WRITEME
	 */
	public void setStructure (final HomeDecorItem item) {
		for (final InventoryItem each : this
				.getItemsByType ("structure")) {
			if (each.getTypeID () == item.getTypeID ()) {
				each.setOwner (this);
				each.setActive (false);
			}
		}
		item.setOwner (this);
		item.setActive (true);
	}

	/**
	 * Set the values specific to a subclass of User
	 * 
	 * @param resultSet WRITEME
	 * @throws SQLException WRITEME
	 */
	protected void setSubclassValues (final ResultSet resultSet)
	throws SQLException {
		/* No op */
	}

	/**
	 * @param id The item ID of the TootBook theme to set active
	 */
	// public void setTootBookTheme (final int id) {
	// tootBookTheme = (TootBookTheme) InventoryItem.getByID (id);
	// }
	/**
	 * @param newTargetX the targetX to set
	 */
	public void setTargetX (final double newTargetX) {
		targetX = newTargetX;
	}

	/**
	 * Sets the TootBook Theme based upon the name in inventory.
	 * 
	 * @param url the identifier (URL part) of the TootBook Theme to be
	 *            selected
	 * @throws NotFoundException if the URL isn't found or isn't in
	 *             inventory
	 */
	// public void setTootsBookThemeByURL (final String url)
	// throws NotFoundException {
	// for (final TootBookTheme theme : getTootBookThemes ())
	// if (theme.getURL ().equals (url)) {
	// setTootBookTheme (theme.getID ());
	// }
	// throw new NotFoundException (url);
	// }
	/**
	 * @param newTargetY the targetY to set
	 */
	public void setTargetY (final double newTargetY) {
		targetY = newTargetY;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.user.AbstractUser#setTravelRate(double)
	 */
	public void setTravelRate (final double rate) {
		travelRate = rate;
	}

	/**
	 * @param newLogin the userName to set
	 * @throws ForbiddenUserException if the user name is not allowed
	 * @throws AlreadyUsedException if the user name is in-use
	 */
	public void setUserName (final String newLogin)
	throws AlreadyUsedException, ForbiddenUserException {
		User.assertUserNameAvailable (newLogin);
		login = newLogin;
		changed ();
	}

	/**
	 * <p>
	 * Set a user name, requested by the user. (Sends to lifeguards for
	 * approval)
	 * </p>
	 * <p>
	 * Clears needsNaming.
	 * </p>
	 * 
	 * @param userRequestedLogin the userName to set
	 * @throws ForbiddenUserException if the user name isn't allowed
	 * @throws AlreadyUsedException if the user name has already been
	 *             used (or requested)
	 */
	public void setUserNameFromUser (final String userRequestedLogin)
	throws AlreadyUsedException, ForbiddenUserException {
		if (isBanned)
			throw new ForbiddenUserException (userRequestedLogin);
		User.assertUserNameAvailable (userRequestedLogin);
		requestNewUserName (userRequestedLogin);
		setNeedsNaming (false);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.HasVariables#setVariable(java.util.Map.Entry)
	 */
	public void setVariable (final Entry <String, String> var) {
		userVariables.put (var.getKey (), var.getValue ());
	}

	/**
	 * WRITEME: document this method (brpocock, Oct 19, 2009)
	 * 
	 * @param varName WRITEME
	 * @param varValue WRITEME
	 */
	public void setVariable (final String varName, final String varValue) {
		if ("d".equals (varName)) {
			final String parts[] = varValue.split ("~");
			if (parts.length == 5 || parts.length == 6) {
				setX ((int) Double.parseDouble (parts [0]));
				setY ((int) Double.parseDouble (parts [1]));
				setTargetX ((int) Double.parseDouble (parts [2]));
				setTargetY ((int) Double.parseDouble (parts [3]));
				setFacing (parts [4]);
				if (parts.length == 5) {
					setLastActive ();
				} else {
					lastActive = new Timestamp ((long) Double
							.parseDouble (parts [5]));
				}
			}
		}

		userVariables.put (varName, varValue);

		getServerThread ().tattle (
				"userVar\t" + varName + "\t" + varValue);
		if (null == getRoom ()) {
			getServerThread ().tattle ("I'm not even nowhere");
		} else if (getRoom ().isLimbo ()) {
			getServerThread ().tattle (
			"Keeping user vars a secret (in Limbo)");
		} else {
			getServerThread ().tattle (
					"Notifying " + getRoom ().getUserCount ()
					+ " folks");
			for (final AbstractUser somebody : getRoom ()
					.getAllUsers ()) {
				getServerThread ().tattle (
						"Telling " + somebody.getAvatarLabel ()
						+ " about my " + varName);
				try {
					if ( !somebody.isNPC ()) {
						final AppiusClaudiusCaecus someThread = ((User) somebody)
						.getServerThread ();
						if (null != someThread) {
							someThread.sendUserVariable (this, varName,
									varValue);
						}
					}
				} catch (final UserDeadException e) {
					// nifty, they're dead!
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.types.HasVariables#setVariables(java.util.Map)
	 */
	public void setVariables (final Map <String, String> map) {
		for (final Entry <String, String> var : map.entrySet ()) {
			this.setVariable (var);
		}
	}

	/**
	 * @param newX the x to set
	 */
	public void setX (final double newX) {
		x = newX;
	}

	/**
	 * @param newY the y to set
	 */
	public void setY (final double newY) {
		y = newY;
	}

	/**
	 * @see org.starhope.appius.user.AbstractUser#speak(org.starhope.appius.game.AbstractRoom,
	 *      java.lang.String)
	 */
	public void speak (final AbstractRoom room, final String string) {
		final JSONObject jso = new JSONObject ();
		try {
			jso.put ("speech", string);
			Commands.do_speak (jso, this, room);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a JSONException in speak", e);
		} catch (final NotFoundException e) {
			AppiusClaudiusCaecus.reportBug (
					"Caught a NotFoundException in speak", e);
		}
	}


	/**
	 * @param userEnrolment the enrolment to be started for this user
	 */
	public abstract void startEnrolment (UserEnrolment userEnrolment);

	/**
	 * This is analogous to {@link #toSFSXML()} for the pure JSON “to
	 * infinity and beyond” interface
	 * 
	 * @return the brief JSON record giving the user's name, ID, and
	 *         variables.
	 */
	public JSONObject toJSONRef () {
		final JSONObject jso = new JSONObject ();
		try {
			jso.put ("name", getAvatarLabel ());
			jso.put ("id", getUserID ());
			final JSONObject vars = new JSONObject ();
			for (final Entry <String, String> var : getUserVariables ()
					.entrySet ()) {
				vars.put (var.getKey (), var.getValue ());
			}
			jso.put ("vars", vars);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		return jso;
	}

	/**
	 * @return The User record in the sense of Smart Fox Server's XML
	 *         format, as used in joinOK and uER (user enters room)
	 *         messages
	 */
	public String toSFSXML () {
		final StringBuilder reply = new StringBuilder ();
		reply.append ("<u i='");
		reply.append (getUserID ());
		reply.append ("' m='0'><n><![CDATA[");
		reply.append (getUserName ().toLowerCase (Locale.ENGLISH));
		reply.append ("]]></n><vars>");
		for (final Entry <String, String> var : getUserVariables ()
				.entrySet ()) {
			reply.append ("<var n='");
			reply.append (var.getKey ());
			reply.append ("' t='s'><![CDATA[");
			reply.append (var.getValue ());
			reply.append ("]]></var>");
		}
		reply.append ("</vars></u>");
		return reply.toString ();
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString () {
		final StringBuilder result = new StringBuilder ();
		result.append ("\nactiveClothing = "
				+ getActiveClothing ().toString ());
		result.append ("\nageGroup = " + getAgeGroup ().toString ());
		result.append ("\napprovedDate = " + getApprovedDateString ());
		result.append ("\navatarClass = "
				+ getAvatarClass ().toString ());
		result.append ("\nbaseColor = " + getBaseColor ().toString ());
		final Date birthdate = getBirthDate ();
		result.append ("\nbirthDate = "
				+ (birthdate.getTime () > 100 ? birthdate.toString ()
						: "undefined"));
		result.append ("\nconnectedFrom = " + getConnectedFrom ());
		result.append ("\nlanguage _ dialect = " + getLanguage ()
				+ " _ " + getDialect ());
		result.append ("\ndisplayName = " + getDisplayName ());
		result.append ("\neMail Plus Date = "
				+ getEmailPlusDateString ());
		result
		.append ("\nextraColor = "
				+ getExtraColor ().toString ());
		result.append ("\ngivenName = " + getGivenName ());
		result.append ("\nhistory = TODO");
		result.append ("\nkicked ( by " + getKickedByUserID ()
				+ " until " + getKickedUntil () + " for "
				+ getKickedReasonCode () + " ) " + getKickedMessage ());
		result.append ("\nmail = " + getMail ());
		result.append ("\nmailConfirmed = " + getMailConfirmed ());
		result.append ("\nnameApprovedAt = " + getNameApprovedAt ()
				+ "; nameApprovedBy = " + getNameApprovedByUserID ());
		result.append ("\nnameRequestedAt = " + getNameRequestedAt ()
				+ "; requestedName = " + getRequestedName ());
		final Parent p = getParent ();
		result.append ("\nparent = "
				+ (null == p ? "null" : getParent ().toString ()));
		result.append ("\npassword = " + getPassword ());
		// result.append ("\npeanuts = " + getPeanuts ().toString ());
		result.append ("\npublicInfo = " + getPublicInfo ());
		result.append ("\nresponsibleMail = ");
		final String responsibleMailAddress = getResponsibleMail ();
		if (null == responsibleMailAddress) {
			result.append ("(null)");
		} else {
			result.append (responsibleMailAddress);
		}
		result.append ("\nstaffLevel = " + getStaffLevel ());
		// result.append ("\ntootTime = " + getTootTimeLeft () + ":"
		// + getTootTimeLeftMinutes ());
		// result.append ("\ntootTime = " + getTootTimeRefill () +
		// " per "
		// + (isTootTimerDay () ? "day" : "month"));
		result.append ("\nuserEnrolments = "
				+ LibMisc.listToDisplay (getUserEnrolments (), "en",
				"US"));
		result.append ("\nuserID = " + getUserID ());
		result.append ("\nuserName = " + getUserName ());
		result.append ("\nactive = " + (isActive () ? "Y" : "N"));
		result.append ("\napproved = " + (isApproved () ? "Y" : "N"));
		result.append ("\nbanned = " + (isBanned () ? "Y" : "N"));
		result.append ("\nbirthday = " + (isBirthday () ? "Y" : "N"));
		result.append ("\ncanceled = " + (isCanceled () ? "Y" : "N"));
		result.append ("\ncanContact = "
				+ (isCanContact () ? "Y" : "N"));
		result.append ("\ncanEnterChatZone = "
				+ (isCanEnterChatZone () ? "Y" : "N"));
		result.append ("\ncaneEnterMenuZone = "
				+ (isCanEnterMenuZone () ? "Y" : "N"));
		result.append ("\ncanTalk = " + (isCanTalk () ? "Y" : "N"));
		result.append ("\nkicked = " + (isKicked () ? "Y" : "N"));
		result.append ("\nneedsNaming = "
				+ (needsNaming () ? "Y" : "N"));
		result.append ("\nneedsParentAttention = "
				+ (isNeedsParentAttention () ? "Y" : "N"));
		result.append ("\nonline = " + (isOnline () ? "Y" : "N"));
		result.append ("\npaidMember = "
				+ (isPaidMember () ? "Y" : "N"));
		result.append ("\nnameNeedsApproval = "
				+ (nameNeedsApproval () ? "Y" : "N"));
		result.append ("\nnameNeedsParentalApproval = "
				+ (nameNeedsParentalApproval () ? "Y" : "N"));
		result.append ("\nparentApprovedName = "
				+ (getParentApprovedName () ? "Y" : "N"));
		result.append ("\npassword recovery Q&A = "
				+ getForgotPasswordQuestion () + " "
				+ getForgotPasswordAnswer ());
		return result.toString ();
	}

	/**
	 * Update the Whirley cache to know about this object.
	 */
	public void updateCache () {
		User.dumpLiveCache ();

		if ( !isOnline ()) {
			// System.out.println (" Hey, I think I'm user #" + userID
			// + " “" + userName
			// + ",” but I don't think I'm online.");
			if (null != User.liveCache.get ("uid=" + userID)) {
				System.out
				.println (" The walrus and the carpenter went walking, though.\007\n");
			}
			return;
		}

		if (null != User.liveCache && isOnline ()) {
			final User dupe = User.liveCache.get ("uid=" + userID);
			if (null != dupe && null != dupe.getIpAddress ()) {
				// yes, this is actually important.
				if ( !dupe.getIpAddress ().equals (getIpAddress ())) {
					final AppiusClaudiusCaecus thread = dupe
					.getServerThread ();
					if (null != thread) {
						thread.disconnectDuplicate ();
					}
				}
			}

			final User removedUID = User.liveCache.remove ("uid="
					+ userID);
			final User removedLog = User.liveCache.remove ("log="
					+ login.toLowerCase (Locale.ENGLISH));

			if (AppiusConfig
					.getConfigBoolOrFalse ("org.starhope.appius.dumpLiveCache")) {
				System.out.println ("liveCache\tremoved by UID: "
						+ (null == removedUID ? "null" : removedUID
								.getUserName ()
								+ " S#" + removedUID.getMySerial ()));
				System.out.println ("liveCache\tremoved by Login: "
						+ (null == removedLog ? "null" : removedLog
								.getUserName ()
								+ " S#" + removedLog.getMySerial ()));
				System.out.println ("liveCache\tstoring UID=" + userID
						+ " login="
						+ login.toLowerCase (Locale.ENGLISH) + " S#"
						+ getMySerial ());
			}

			User.liveCache.put ("uid=" + userID, this);
			User.liveCache.put ("log="
					+ login.toLowerCase (Locale.ENGLISH), this);
			User.dumpLiveCache ();
		}
		if (null != User.userCache) {
			User.userCache.remove ("uid=" + userID);
			User.userCache.remove ("log="
					+ login.toLowerCase (Locale.ENGLISH));

			User.userCache.store ("uid=" + userID, this);
			User.userCache.store ("log="
					+ login.toLowerCase (Locale.ENGLISH), this);
		}
	}

	/**
	 * @param item The clothing item to be worn
	 */
	public void wear (final ClothingItem item) {
		this.wear (item, -1);
	}

	/**
	 * Put on a wearable item, to include Pivitz as well as clothing and
	 * so forth.
	 * 
	 * @param item The item to be worn
	 * @param colour The color to set for the item. If the colour is not
	 *            being overridden, set this to null.
	 */
	public void wear (final ClothingItem item, final Colour colour) {
		blog ("Putting on clothes, item " + item.getID () + " (colour "
				+ colour + ")");

		final Connection conn;
		try {
			conn = AppiusConfig.getDatabaseConnection ();
			// conn.setAutoCommit (false);
		} catch (final SQLException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}

		PreparedStatement doff = null;
		PreparedStatement don = null;
		try {
			if (item.isADress ()) {
				// Remove shirts, pants, and dresses
				doff = conn
				.prepareStatement ("UPDATE inventory LEFT JOIN items ON inventory.itemID = items.id SET inventory.isActive='N' WHERE items.itemTypeID IN (9, 14, 10) AND inventory.userID = ?");
				doff.setInt (1, userID);
				doff.executeUpdate ();
			} else if (item.isAShirt ()) {
				// Remove shirts and dresses
				doff = conn
				.prepareStatement ("UPDATE inventory LEFT JOIN items ON inventory.itemID = items.id SET inventory.isActive='N' WHERE items.itemTypeID IN (9, 14) AND inventory.userID = ?");
				doff.setInt (1, userID);
				doff.executeUpdate ();
			} else if (item.isPants ()) {
				// Remove pants and dresses
				doff = conn
				.prepareStatement ("UPDATE inventory LEFT JOIN items ON inventory.itemID = items.id SET inventory.isActive='N' WHERE items.itemTypeID IN (10, 14) AND inventory.userID = ?");
				doff.setInt (1, userID);
				doff.executeUpdate ();
			} else {
				doff = conn
				.prepareStatement ("UPDATE inventory LEFT JOIN items ON inventory.itemID = items.id SET inventory.isActive='N' WHERE items.itemTypeID = ? AND inventory.userID = ?");
				doff.setInt (1, item.getTypeID ());
				doff.setInt (2, userID);
				blog (doff.toString ());
				doff.executeUpdate ();
			}

			don = conn
			.prepareStatement ("UPDATE inventory SET isActive='Y', color=? WHERE itemID=? AND userID=?");
			if (null != colour) {
				don.setInt (1, (int) colour.toLong ());
			} else {
				don.setNull (1, Types.INTEGER);
			}
			don.setInt (2, item.getID ());
			don.setInt (3, userID);
			blog (" don.executeUpdate() => " + don.executeUpdate ());

			// conn.commit ();
			fetch_inventory ();
			updateCache ();
		} catch (final SQLException e) {
			try {
				conn.rollback ();
			} catch (final SQLException e1) {
				AppiusClaudiusCaecus.reportBug ("rollback failed", e1);
			}
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			if (null != don) {
				try {
					don.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
			if (null != doff) {
				try {
					doff.close ();
				} catch (final SQLException e) { /* No Op */
				}
			}
		}

	}

	/**
	 * WRITEME: document this method (brpocock, Jan 12, 2010)
	 * 
	 * @param item WRITEME
	 * @param color WRITEME
	 */
	public void wear (final ClothingItem item, final int color) {
		if ( -1 == color) {
			wear (item, null);
		} else {
			wear (item, new Colour (color));
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.via.ViaAppia#writeExternal(java.io.ObjectOutput)
	 */
	public void writeExternal (final ObjectOutput out)
	throws IOException {
		// XXX Auto-generated method stub (brpocock, Dec 9, 2009)

	}

}
