/**
 * <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
 */
package org.starhope.appius.game;

import java.io.*;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListSet;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.AlreadyUsedException;
import org.starhope.appius.except.ForbiddenUserException;
import org.starhope.appius.except.NotFoundException;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.messaging.Mail;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.types.ProtocolState;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.Person;
import org.starhope.appius.user.User;
import org.starhope.appius.util.AcceptsMetronomeTicks;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;
import org.starhope.util.types.CanProcessCommands;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * <p>
 * Appius Claudius Caecus is a game server application framework.
 * Extensible using Java classes loaded from its extensive library of
 * configurable options, Appius provides a network server for real-time
 * data exchange.
 * </p>
 * <p>
 * Originally titled “Braque,” this application was developed for use
 * with the videogame (work in progress) “Sideres.” It was designed to
 * operate with a messaging protocol called “Cubist,” which has since
 * been defined in terms of a simple JSON + \0 wire protocol.
 * </p>
 * <p>
 * Since that time, I have repurposed the game engine for use with
 * Tootsville™ (http://www.Tootsville.com/) and set up a new series of
 * communications supports designed to be compatible with the
 * ActionScript 3 libraries for Smart Fox Server Pro.
 * </p>
 * <p>
 * The server should still be able to operate on its own with minimal
 * changes. Most of the Tootsville-specific code has been isolated into
 * the com.tootsville.* package, but replacement code for some methods
 * might be needed to create a stand-alone game. Also, some default
 * configuration values are Tootsville-oriented.
 * </p>
 * <p>
 * This class in general operates as both the main game thread and
 * socket listener — in the static methods and variables — and the
 * client-connected user thread — in the instance methods and variables.
 * The “metronome” timer thread also runs using the static methods of
 * this class.
 * </p>
 * 
 * @author brpocock
 */
public class AppiusClaudiusCaecus extends Thread implements
AcceptsMetronomeTicks, UncaughtExceptionHandler,
CanProcessCommands, Comparable <AppiusClaudiusCaecus> {
	/**
	 * Prepared statement used to inject log entries into the log
	 */
	private static PreparedStatement blatherStatement;

	/**
	 * The time at which the server was started
	 */
	private static final long bootTime = System.currentTimeMillis ();

	/**
	 * Global reaper thread to reap zombies
	 */
	final static Charon charon = new Charon ();

	/**
	 * The delay before the first run of the NPC manager task.
	 */
	public static final int DELAY_MS = 5000;

	/**
	 * The most users who have been online since the server started
	 */
	private static int highWaterUsers = 0;

	/**
	 * The date format used for system messages. A constant.
	 */
	final static SimpleDateFormat isoDate = new SimpleDateFormat (
	"yyyy-MM-dd HH:mm:ss");

	/**
	 * The connection to the journal database, used by blather
	 */
	private static Connection journalDB;
	/**
	 * If this ever transitions to “false,” stop listening for new
	 * connections. Global kill switch.
	 */
	private static boolean keepListening = true;
	/**
	 * If the journaling database goes offline, this variable will keep
	 * from getting a flood of bug reports. On every successful write to
	 * the journal database, this flag is reset to false. However, after
	 * any failure, it flip-flops to true, allowing only one bug report
	 * to be mailed at a time.
	 */
	private static boolean knowWhyJournalDBOut = false;
	/**
	 * The time at which the global metronome thread last ticked.
	 */
	private static long lastMetronomeTick = System.currentTimeMillis ();

	/**
	 * Users logging in are directed first the this landing zone, and
	 * then choose a Zone server to which they wish to connect (if
	 * multiple Zones have been established).
	 */
	static Zone loginZone;
	/**
	 * The timer driving the global metronome.
	 */
	static Timer metronome;

	/**
	 * Collection of arbitrary objects who wish to receive Metronome
	 * ticks
	 */
	private static ConcurrentSkipListSet <AcceptsMetronomeTicks> metronomeListeners = new ConcurrentSkipListSet <AcceptsMetronomeTicks> ();

	/**
	 * The global metronome thread.
	 */
	static Thread metronomeThread;
	/**
	 * Message of the day
	 */
	private static String motd = "";

	/**
	 * The listening port for the server
	 */
	private static int port = 2770;
	/**
	 * The version of the serialized form of this class.
	 */
	private static final long serialVersionUID = 3936846581752697097L;
	/**
	 * All live server threads
	 */
	private static ConcurrentHashMap <String, AppiusClaudiusCaecus> serverThreads = new ConcurrentHashMap <String, AppiusClaudiusCaecus> ();
	/**
	 * A global boolean flag to indicate that the server has started up
	 * successfully
	 */
	private static boolean started;

	/**
	 * Time at which the Game State pump last ran
	 */
	private static long tGameStatePump;

	/**
	 * Time at which the server statistics were last “reportBug” mailed
	 */
	private static long tStats = 0;

	/**
	 * The number of milliseconds in 21 years. Used for autovivification
	 * of accounts.
	 */
	private static final int TWENTY_ONE_YEARS_MSEC = 365 * 21 * 60 * 60
	* 1000;

	/**
	 * All zones active on this server
	 */
	private static ConcurrentHashMap <String, Zone> zones = new ConcurrentHashMap <String, Zone> ();

	/**
	 * Add a thread to the Metronome tick event schedule without that
	 * thread being
	 * 
	 * @param listener The listener who wishes to receive Metronome
	 *            ticks.
	 */
	public static void add (final AcceptsMetronomeTicks listener) {
		AppiusClaudiusCaecus.metronomeListeners.add (listener);
	}

	/**
	 * Add a Zone to the global Zones list
	 * 
	 * @param zone the Zone to be added
	 */
	public static void add (final Zone zone) {
		AppiusClaudiusCaecus.zones.put (zone.getName (), zone);
	}

	/**
	 * @param zoneName The zone's name
	 * @param zone The Zone object
	 */
	@Deprecated
	public static void addZone (final String zoneName, final Zone zone) {
		AppiusClaudiusCaecus.zones.put (zoneName, zone);
	}

	/**
	 * Print a debugging message at low urgency, from a random place
	 * 
	 * @param message The message
	 */
	public static void blather (final String message) {
		AppiusClaudiusCaecus.blather ("", "", "", message, false);
	}

	/**
	 * Write out a log message to the log file and/or database
	 * 
	 * @param user The user name and ID responsible
	 * @param room The room and zone in which the action occurred
	 * @param address The user's remote socket's IP address and port
	 *            number
	 * @param message The event which occurred
	 * @param urgent Urgent messages are written even when debugging is
	 *            disabled
	 */
	public static void blather (final String user, final String room,
			final String address, final String message,
			final boolean urgent) {
		if (false == urgent && !AppiusConfig.isDebug ()) return;
		final StringBuilder blatherscythe = new StringBuilder ();
		blatherscythe.append (new java.util.Date ().toString ());
		blatherscythe.append ('\t');
		blatherscythe.append (user);
		blatherscythe.append ('\t');
		blatherscythe.append (room);
		blatherscythe.append ('\t');
		blatherscythe.append (address);
		blatherscythe.append ('\t');
		blatherscythe.append (message);
		blatherscythe.append ('\n');
		System.err.print (blatherscythe.toString ());
		System.err.flush ();
		if ( !AppiusClaudiusCaecus.started) return;
		try {
			if (false == urgent) return;
			if (null == AppiusClaudiusCaecus.journalDB
					|| AppiusClaudiusCaecus.journalDB.isClosed ()) {
				AppiusClaudiusCaecus.journalDB = AppiusConfig
				.getJournalDatabaseConnection ();
				AppiusClaudiusCaecus.blatherStatement = AppiusClaudiusCaecus.journalDB
				.prepareStatement ("INSERT DELAYED INTO appiusBlather (stamp, user, room, address, message) VALUES (NOW(),?,?,?,?)");
			}
			synchronized (AppiusClaudiusCaecus.blatherStatement) {
				AppiusClaudiusCaecus.blatherStatement.setString (1,
						user);
				AppiusClaudiusCaecus.blatherStatement.setString (2,
						room);
				AppiusClaudiusCaecus.blatherStatement.setString (3,
						address);
				AppiusClaudiusCaecus.blatherStatement.setString (4,
						message);
				if (AppiusClaudiusCaecus.blatherStatement.execute ()) {
					AppiusClaudiusCaecus.knowWhyJournalDBOut = false;
				}
			}
		} catch (final SQLException e) {
			System.err.println ("> Journal DB offline\tfrom "
					+ Thread.currentThread ().getName () + "\n");
			if ( !AppiusClaudiusCaecus.knowWhyJournalDBOut) {
				AppiusClaudiusCaecus.reportBug (
						"Journal database offline (single report)", e);
				AppiusClaudiusCaecus.knowWhyJournalDBOut = true;
			}
			return;
		}

	}

	/**
	 * Write out an error message to the log file and/or mail, as
	 * appropriate
	 * 
	 * @param subject The subject
	 * @param message The error message
	 */
	public static void bugDuplex (final String subject,
			final String message) {
		if (AppiusConfig.mailBugs ()) {
			AppiusConfig.setMailBugs (false);
			Mail.sendBugReport (subject, message);
			AppiusConfig.setConfig ("org.starhope.appius.mailBugs",
			"true");
		}
		// AbstractWebLocation abstractWebLocation = new
		// AbstractWebLocation
		// ("http://goethe.tootsville.com/trac.cgi";
		// Version version = xxx;
		// TracClientFactory.createClient(abstractWebLocation, version)

		/*
		 * try { Mail.sendTemplateMail (User.getByLogin ("Catvlle"),
		 * true, "bugReport", "Bug Report from Appius", null, reason +
		 * "\n\n" + e.toString ()); } catch (final FileNotFoundException
		 * e1) { System.err .println
		 * ("Can't write bug report to mail: Template bugReport is missing"
		 * ); } catch (final IOException e1) { System.err .println
		 * ("Can't write bug report to mail: I/O Exception"); } catch
		 * (final NotFoundException e1) { System.err .println
		 * ("Can't write bug report to mail: User not found"); }
		 */

		System.err.println (message);
		System.err.flush ();
	}

	/**
	 * 
	 */
	public static void configUpdated () {
		AppiusClaudiusCaecus.blather ("", "", "",
				"* Configuration reloaded", true);
		final int newPort = AppiusConfig.getIntOrDefault (
				"org.starhope.appius.server.port", 2770);
		if (AppiusClaudiusCaecus.port != newPort) {
			AppiusClaudiusCaecus.port = newPort;
			AppiusClaudiusCaecus.blather ("Changing port number to "
					+ newPort);
			AppiusClaudiusCaecus.keepListening = false;
		}
	}

	/**
	 * The exception passed in will be reported, as per reportBug, and
	 * then re-thrown as an Error, killing the process responsible
	 * 
	 * @param e An exception to report
	 * @return the error (in theory), which can't be used, but makes the
	 *         source code more legible.
	 * @throws Error (based upon the exception) every time. Since the
	 *             Error is also thrown, it's never actually received by
	 *             the caller, but it makes the compiler happy to write
	 *             it into a throw clause, so it realizes that the flow
	 *             will not continue.
	 */
	public static Error fatalBug (final Exception e) {
		AppiusClaudiusCaecus.reportBug (e);
		throw new Error (e);
	}

	/**
	 * Report a bug, and throw a fatal Error.
	 * 
	 * @param string The error to report
	 * @return a copy of the Error. Since the Error is also thrown, it's
	 *         never actually received by the caller, but it makes the
	 *         compiler happy to write it into a throw clause, so it
	 *         realizes that the flow will not continue.
	 */
	public static Error fatalBug (final String string) {
		return AppiusClaudiusCaecus.fatalBug (new Exception (string));
	}

	/**
	 * Report a bug, and throw a fatal Error.
	 * 
	 * @param string A description of the error to report
	 * @param t An exception to escalate to the Error
	 * @return a copy of the Error. Since the Error is also thrown, it's
	 *         never actually received by the caller, but it makes the
	 *         compiler happy to write it into a throw clause, so it
	 *         realizes that the flow will not continue.
	 */
	public static Error fatalBug (final String string, final Throwable t) {
		AppiusClaudiusCaecus.reportBug (string, t);
		throw new Error (t);
	}

	/**
	 * Get the accurate number of users in all zones.
	 * 
	 * @return The number of users in all zones.
	 */
	private static int getAccurateHeadcount () {
		int sum = 0;
		for (final Zone z : AppiusClaudiusCaecus.zones.values ()) {
			if ( !z.getName ().startsWith ("$")) {
				sum += z.getAllUsersIDsInZone ().size ();
			}
		}
		return sum;
	}

	/**
	 * Get a collection of all users in all zones.
	 * 
	 * @return all users in all Zones
	 */
	public static Collection <AbstractUser> getAllUsers () {
		final HashSet <AbstractUser> everybody = new HashSet <AbstractUser> ();
		for (final AbstractZone z : AppiusClaudiusCaecus.getAllZones ()) {
			everybody.addAll (z.getAllUsersInZone ());
		}
		return everybody;
	}

	/**
	 * @return All active zones in the multiverse
	 */
	public static LinkedList <AbstractZone> getAllZones () {
		return new LinkedList <AbstractZone> (
				AppiusClaudiusCaecus.zones.values ());
	}

	/**
	 * Get the server start time
	 * 
	 * @return The time at which the server started in milliseconds
	 *         since epoch
	 */
	public static long getBootTime () {
		return AppiusClaudiusCaecus.bootTime;
	}

	/**
	 * Get the {@link Charon} reaper thread
	 * 
	 * @return Charon
	 */
	static Charon getCharon () {
		return AppiusClaudiusCaecus.charon;
	}

	/**
	 * Get the high-water mark count of users
	 * 
	 * @return the highest number of simultaneous users who have been
	 *         online since server boot
	 */
	public static int getHighWaterUsers () {
		AppiusClaudiusCaecus.updateHighWaterMark ();
		return AppiusClaudiusCaecus.highWaterUsers;
	}

	/**
	 * @return the time at which the metronome last ticked
	 */
	public static long getLastMetronomeTick () {
		return AppiusClaudiusCaecus.lastMetronomeTick;
	}

	/**
	 * @return the motd
	 */
	public static String getMOTD () {
		return AppiusClaudiusCaecus.motd;
	}

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

	/**
	 * Get the hostname on which the server process is running
	 * 
	 * @return the hostname of the local host
	 */
	public static String getServerHostname () {
		try {
			return InetAddress.getLocalHost ().getHostName ();
		} catch (final UnknownHostException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
	}

	/**
	 * The server (TCP) port number
	 * 
	 * @return the port on which the server should listen
	 */
	public static int getServerPort () {
		return AppiusClaudiusCaecus.port;
	}

	/**
	 * <p>
	 * This extracts a stack backtrace from a Throwable into a string
	 * format for a bug report. Each line is tagged with a leading "/#"
	 * string, followed by a space, the stack backtrace element, and a
	 * newline.
	 * </p>
	 * <p>
	 * This is the same as calling
	 * {@link #getStackTrace(Throwable, String)} with a prefix of "\n/#"
	 * </p>
	 * 
	 * @param throwable The Throwable containing stack backtrace data
	 * @return A string representation of the backtrace, suitable for
	 *         logging or bug reports.
	 */
	private static String getStackTrace (final Throwable throwable) {
		return AppiusClaudiusCaecus.getStackTrace (throwable, "\n/# ")
		+ "\n";
	}

	/**
	 * This extracts a stack backtrace from a Throwable into a string
	 * format for a bug report. Each line is preceded by the supplied
	 * prefix string, followed the stack backtrace element. The string
	 * does not end with a newline.
	 * 
	 * @param throwable A {@link Throwable} from which to extract a
	 *            stack trace
	 * @param prefix The string with which to separate lines of the
	 *            trace.
	 * @return The stacktrace as a string
	 */
	private static String getStackTrace (final Throwable throwable,
			final String prefix) {
		int watchdog = 0;
		final StringBuilder stackTrace = new StringBuilder ();
		if (null == throwable) return "";
		for (final StackTraceElement element : throwable
				.getStackTrace ()) {
			stackTrace.append (prefix);
			stackTrace.append (element.toString ());
			if (200 < ++watchdog) {
				stackTrace.append (prefix);
				stackTrace
				.append ("... and many more (limiter invoked)");
			}
		}
		return stackTrace.toString ();
	}

	/**
	 * Returns the number of server threads running. There are more
	 * threads in the VM, because there are also bookkeeping threads,
	 * the main listener, the metronome, etc.
	 * 
	 * @return The number of server threads running
	 */
	public static int getThreadCount () {
		AppiusClaudiusCaecus.updateHighWaterMark ();
		return AppiusClaudiusCaecus.serverThreads.size ();
	}

	/**
	 * Find a Zone object for a given zone name
	 * 
	 * @param zoneName The name of the zone to be found
	 * @return The Zone object, if the selected Zone exists; else, null
	 */
	public static Zone getZone (final String zoneName) {
		return AppiusClaudiusCaecus.zones.get (zoneName);
	}

	/**
	 * <p>
	 * Listen for incoming connections, and sit here until we get one.
	 * </p>
	 * <p>
	 * If keepListening is altered from elsewhere, then the loop will
	 * restart; probably because the port number has been changed.
	 * </p>
	 */
	private static void listenForever () {
		ServerSocket serverSocket = null;
		try {
			serverSocket = new ServerSocket (AppiusClaudiusCaecus.port);
			AppiusClaudiusCaecus.keepListening = true;
		} catch (final IOException e) {
			AppiusClaudiusCaecus.blather ("", "", ":"
					+ AppiusClaudiusCaecus.port,
					"Could not listen on port: "
					+ AppiusClaudiusCaecus.port, true);
			Runtime.getRuntime ().exit ( -1);
		}

		if (null == serverSocket)
			throw AppiusClaudiusCaecus
			.fatalBug ("Can't listen for connections: No server socket acquired");
		int size;
		try {
			size = AppiusConfig
			.getInt ("org.starhope.appius.socket.receiveBufferSize");
			serverSocket.setReceiveBufferSize (size);
		} catch (final NumberFormatException e1) {
			AppiusClaudiusCaecus.fatalBug (e1);
		} catch (final NotFoundException e1) {
			// ignore
		} catch (final SocketException e) {
			AppiusClaudiusCaecus.fatalBug (e);
		}

		try {
			AppiusClaudiusCaecus
			.blather ("Server socket receive buffer: "
					+ serverSocket.getReceiveBufferSize ());
		} catch (final SocketException e1) {
			System.err
			.println ("Can't get size of server socket receive buffer");
		}

		AppiusClaudiusCaecus.loginZone.handleServerReady ();

		while (AppiusClaudiusCaecus.keepListening) {
			AppiusClaudiusCaecus.blather ("", "", serverSocket
					.getLocalSocketAddress ().toString (),
					"Listening on port " + AppiusClaudiusCaecus.port
					+ " for clients", true);

			try {
				final Socket userSocket = serverSocket.accept ();
				AppiusClaudiusCaecus.blather ("", "", userSocket
						.getRemoteSocketAddress ().toString (),
						"Accepting new connection", false);
				new AppiusClaudiusCaecus (userSocket,
						AppiusClaudiusCaecus.loginZone).start ();

				if (AppiusClaudiusCaecus.lastMetronomeTick > 0
						&& AppiusConfig
						.getConfigBoolOrFalse ("org.starhope.appius.autoRestartMetronome")
						&& AppiusClaudiusCaecus.lastMetronomeTick
						+ AppiusConfig.getMetronomeTime () * 3 > System
						.currentTimeMillis ()) {
					AppiusClaudiusCaecus
					.reportBug ("Metronome had not ticked in "
							+ (System.currentTimeMillis () - AppiusClaudiusCaecus.lastMetronomeTick)
							+ "ms, restarting metronome");
					AppiusClaudiusCaecus.restartMetronome ();
				}
			} catch (final IOException e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}

	}

	/**
	 * Record an event to the journal
	 * 
	 * @param verb The verb describing the event
	 * @param zoneName The zone in which it occurred
	 * @param userName The user causing the action
	 * @param targetName The target of the action, if any
	 * @param details Additional details
	 */
	public static void logEvent (final String verb,
			final String zoneName, final String userName,
			final String targetName,
			final HashMap <String, String> details) {
		PreparedStatement st = null;
		Connection con = null;
		try {
			con = AppiusConfig.getDatabaseConnection ();
			st = con
			.prepareStatement ("INSERT DELAYED INTO journal (verb, zone, user, object, stamp) VALUES (?,?,?,?, NOW())");
			st.setString (1, verb);
			st.setString (2, zoneName);
			st.setString (3, userName);
			st.setString (4, targetName);
			st.execute ();
		} catch (final SQLException e) {
			System.err
			.println ("WARNING: Can't write entry to journal\n VERB: "
					+ verb
					+ "\n TIME: "
					+ new Date (System.currentTimeMillis ())
					.toString ()
					+ " ZONE: "
					+ zoneName
					+ " USER: "
					+ userName
					+ "\n TARGET: "
					+ targetName + "\n\n");
		} 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
				}
			}
		}
		if (null != details) {
			System.err
			.println ("WARNING: Can't write details to journal yet");
		}

	}

	/**
	 * This is the main routine to run Appius as a stand-alone server.
	 * 
	 * @param argv Any command-line arguments
	 * @throws IOException if there's an I/O exception
	 */
	public static void main (final String [] argv) throws IOException {
		Thread.currentThread ().setName ("AppiusClaudiusCaecus.main");
		AppiusClaudiusCaecus.port = 2770;

		int argc = 0;
		while (argv.length > argc) {
			final String arg = argv [argc++ ];
			if (arg.equals ("-p")) {
				final String pNum = argv [argc++ ];
				AppiusClaudiusCaecus.port = Integer.parseInt (pNum);
				if (AppiusClaudiusCaecus.port < 1024) {
					System.err
					.println ("Usage: -p <PORT> — port number required");
					Runtime.getRuntime ().exit ( -1);
				}
			}
		}

		AppiusClaudiusCaecus.charon.start ();

		System.out.println ("\n\n Good morning. Appius here.\n"
				+ " ^^^^ ^^^^^^^  ^^^^^^ ^^^^\n\n"
				+ "(Standard output)");
		System.err.println ("\n\n Good morning. Appius here.\n"
				+ " ^^^^ ^^^^^^^  ^^^^^^ ^^^^\n\n"
				+ "(Standard error)\n\n"
				+ "Date\tUser\tRoom\tAddress\tMessage");

		AppiusClaudiusCaecus.loginZone = new Zone ("$Eden");

		AppiusClaudiusCaecus.loginZone.init ();
		AppiusClaudiusCaecus.loginZone.activate ();

		AppiusClaudiusCaecus.port = AppiusConfig.getIntOrDefault (
				"org.starhope.appius.server.port", 2770);

		Thread
		.setDefaultUncaughtExceptionHandler (new AppiusClaudiusCaecus (
		"AppiusClaudiusCaecus.uncaughtExceptionHandler"));

		AppiusClaudiusCaecus.startMetronome ();

		if (AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.openAdmin")) {
			final AdminListener adminListener = new AdminListener ();
			adminListener.start ();
		}

		AppiusClaudiusCaecus.started = true;
		AppiusClaudiusCaecus
		.blather ("Appius Claudius Caecus server started");

		while (true) {
			AppiusClaudiusCaecus.listenForever ();
		}

	}

	/**
	 * <p>
	 * Migrate all users to another host. They will attempt to connect
	 * to identical zones as the ones which are currently active.
	 * </p>
	 * <p>
	 * This is a low-level function and does nothing to ensure that the
	 * given zone actually exists on the other server
	 * </p>
	 * 
	 * @param otherHost the other host to which users should migrate
	 * @param otherPort the other host's listening port to which users
	 *            should migrate
	 */
	public static void migrateAll (final String otherHost,
			final int otherPort) {
		for (final AppiusClaudiusCaecus server : AppiusClaudiusCaecus.serverThreads
				.values ()) {
			final String zoneName = null == server.getZone () ? server
					.getZone ().getName () : "$Eden";
					server.migrate (otherHost, otherPort, zoneName);
		}
	}

	/**
	 * Remove a metronome listener
	 * 
	 * @param listener The listener to deregister
	 */
	public static void remove (final AcceptsMetronomeTicks listener) {
		AppiusClaudiusCaecus.metronomeListeners.remove (listener);
	}

	/**
	 * Remove a zone from the server
	 * 
	 * @param whichZone The zone to be removed
	 */
	public static void remove (final Zone whichZone) {
		AppiusClaudiusCaecus.zones.remove (whichZone.getName ());
	}

	/**
	 * <p>
	 * Report a bug.
	 * </p>
	 * <p>
	 * This is used to catch either ‘impossible things’ or things that
	 * are so bad that immediate programmer intervention is needed.
	 * </p>
	 * <p>
	 * Bug reports should eventually be funneled into the bug-tracking
	 * system or similar automatically, and forwarded to the systems
	 * programmers via eMail and SMS.
	 * </p>
	 * 
	 * @param string Bug report
	 */
	public static void reportBug (final String string) {
		final Throwable t = new Throwable ();
		try {
			throw t;
		} catch (final Throwable blah) {
			AppiusClaudiusCaecus.reportBug (string, blah);
		}
	}

	/**
	 * Report a bug to the automatic bug-tracking systems. This is an
	 * exception which "should never" be thrown, being caught and
	 * referred back for programmer intervention.
	 * 
	 * @param reason The reason this is a bug, if known.
	 * @param throwable The "impossible" exception.
	 */
	public static synchronized void reportBug (final String reason,
			final Throwable throwable) {
		final String stackTrace = AppiusClaudiusCaecus
		.getStackTrace (throwable);

		final StringBuilder bugReport = new StringBuilder ();
		bugReport
		.append ("/* <*>BUG<*>\n\n// Bug report, auto-generated by software.\n/= Reason: ");
		bugReport.append (reason);
		bugReport.append ("\n/= Exception class: ");
		bugReport.append (throwable.getClass ().getCanonicalName ());
		bugReport.append ("\n/= Exception message: ");
		bugReport.append (throwable.getMessage ());
		bugReport.append ("\n/= Exception localized message: ");
		bugReport.append (throwable.getLocalizedMessage ());
		bugReport.append ("\n/= Thread: ");
		bugReport.append (Thread.currentThread ().getName ());
		bugReport.append ("\n/= Stack Backtrace:");
		bugReport.append (stackTrace);
		Throwable cause = throwable.getCause ();
		final int depth = 1;
		while (null != cause) {
			final StringBuilder prefix = new StringBuilder ("\n/");
			for (int i = 0; i < depth; ++i) {
				prefix.append ('/');
			}
			bugReport.append (prefix);
			bugReport.append (" Exception depth: ");
			bugReport.append (depth);
			bugReport.append (prefix);
			bugReport
			.append (" Exception was caused by another exception");
			bugReport.append (prefix);
			bugReport.append ("= Caused by exception class: ");
			bugReport.append (cause.getClass ().getCanonicalName ());
			bugReport.append (prefix);
			bugReport.append ("= Caused by exception message: ");
			bugReport.append (cause.getMessage ());
			bugReport.append (prefix);
			bugReport.append ("= Caused by localized message: ");
			bugReport.append (cause.getLocalizedMessage ());
			bugReport.append (prefix);
			bugReport.append ("= Caused by stack backtrace:");
			bugReport.append (AppiusClaudiusCaecus.getStackTrace (
					throwable, prefix + "# "));
			cause = cause.getCause ();
		}
		bugReport.append ("/= Timestamp Now: "
				+ "\n/= Human Timestamp: ");
		bugReport.append (AppiusClaudiusCaecus.isoDate.format (Calendar
				.getInstance ().getTime ()));
		bugReport.append ("\n/> End automated bug report");
		AppiusClaudiusCaecus.bugDuplex (reason
				+ " — Appius Claudius Caecus", bugReport.toString ());

	}

	/**
	 * @param e An exception to report
	 */
	public static void reportBug (final Throwable e) {
		AppiusClaudiusCaecus.reportBug (e.toString (), e);
	}

	/**
	 * Report a bug from the client application.
	 * 
	 * @param string The client application's bug
	 */
	public static void reportClientBug (final String string) {
		AppiusClaudiusCaecus.reportClientBug (string, null);
	}

	/**
	 * Report a bug from the client application.
	 * 
	 * @param string The client application's bug
	 * @param thread The associated server thread, whose information
	 *            will be prepended, if present. (null is a valid
	 *            answer, for backward compatibility)
	 */
	public static void reportClientBug (final String string,
			final AppiusClaudiusCaecus thread) {
		AppiusClaudiusCaecus.bugDuplex (
				"Client Bug report caught by Appius Claudius Caecus",
				"/* Bug report from client application, auto-generated by software.\n"
				+ (null == thread ? "" : thread.toString ())
				+ string + "\n/> End automated bug report");
	}

	/**
	 * This should restart the server, but it doesn't.
	 */
	public static void restart () {
		AppiusClaudiusCaecus
		.blather ("Restart requested (Unimplemented)");
		// TODO
	}

	/**
	 * Attempt to restart the global metronome — this probably won't
	 * work as currently implemented (?)
	 */
	public static void restartMetronome () {
		try {
			AppiusClaudiusCaecus.stopMetronome ();
		} catch (final Exception e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		AppiusClaudiusCaecus.startMetronome ();
	}

	/**
	 * TODO: document this method (brpocock, Jan 5, 2010)
	 * 
	 * @param string new message of the day
	 */
	public static void setMOTD (final String string) {
		AppiusClaudiusCaecus.motd = string;
	}

	/**
	 * Start the global metronome to ticking
	 */
	public static void startMetronome () {
		AppiusClaudiusCaecus.metronome = new Timer ("metronome", true);
		final TimerTask metronomeTask = new TimerTask () {
			@Override
			public void run () {
				AppiusClaudiusCaecus.tick ();
			}
		};
		AppiusClaudiusCaecus.metronome.scheduleAtFixedRate (
				metronomeTask, AppiusConfig.getMetronomeTime () * 5,
				AppiusConfig.getMetronomeTime ());
	}

	/**
	 * Register an object (usually a server thread) who wishes to begin
	 * accepting metronome ticks.
	 * 
	 * @param thread the thread who wants to accept ticks now
	 */
	private static void startTicking (final AppiusClaudiusCaecus thread) {
		AppiusClaudiusCaecus.blather ("Adding " + thread.getName ()
				+ " to event timer");
		AppiusClaudiusCaecus.serverThreads.put (thread.getName (),
				thread);
		AppiusClaudiusCaecus.updateHighWaterMark ();
	}

	/**
	 * 
	 */
	public static void stopMetronome () {
		try {
			AppiusClaudiusCaecus.metronome.cancel ();
		} catch (final Exception e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		AppiusClaudiusCaecus.metronome = null;
	}

	/**
	 * Stop sending metronome ticks to a particular object. If that
	 * object happens to be a {@link Thread} as well, also sends the
	 * {@link Thread} an {@link Thread#interrupt()}. If it's an instance
	 * of {@link AppiusClaudiusCaecus}, it will first call the thread's
	 * {@link #end()} method.
	 * 
	 * @param thread the object who doesn't want to get any more ticks
	 */
	private static void stopTicking (final AcceptsMetronomeTicks thread) {
		int antiThrash = 10;
		while (AppiusClaudiusCaecus.serverThreads
				.containsValue (thread)) {
			AppiusClaudiusCaecus.blather ("Removing "
					+ thread.getName () + " from event timer…");
			for (final Entry <String, AppiusClaudiusCaecus> ent : AppiusClaudiusCaecus.serverThreads
					.entrySet ()) {
				if (ent.getValue ().equals (thread)) {
					AppiusClaudiusCaecus.blather ("Removing "
							+ thread.getName () + " (" + ent.getKey ()
							+ ")");
					AppiusClaudiusCaecus.serverThreads.remove (ent
							.getKey ());
				}
			}
			if ( --antiThrash < 0)
				throw AppiusClaudiusCaecus
				.fatalBug ("Can't get rid of thread!");
		}
		if (thread instanceof AppiusClaudiusCaecus) {
			((AppiusClaudiusCaecus) thread).end ();
		} else if (thread instanceof Thread) {
			((Thread) thread).interrupt ();
		}
	}

	/**
	 * Create a pure string version of a stack backtrace
	 * 
	 * @param stackTrace An array of StackTraceElements:s
	 * @return A string version of the entire stack backtrace
	 */
	public static String stringify (
			final StackTraceElement [] stackTrace) {
		final StringBuilder s = new StringBuilder ();
		for (final StackTraceElement el : stackTrace) {
			s.append ('\t');
			s.append (el.toString ());
			s.append (el.isNativeMethod () ? "(native)" : "");
			s.append ('\n');
		}
		return s.toString ();
	}

	/**
	 * @param e A Throwable to be stringified into a backtrace
	 * @return The string form
	 */
	public static String stringify (final Throwable e) {
		final StringBuilder s = new StringBuilder ();
		s.append (e.toString () + " " + e.getMessage ());
		s.append ('\n');
		s.append (AppiusClaudiusCaecus.stringify (e.getStackTrace ()));
		return s.toString ();
	}

	/**
	 * The main metronome single-threaded tick
	 */
	public static void tick () {
		final long t = System.currentTimeMillis ();
		final long deltaT = t - AppiusClaudiusCaecus.lastMetronomeTick;
		if (AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.metronome.tick")) {
			System.err.println (" ('-)\ttick\t" + t + "\t" + deltaT);
		}
		final boolean dumpUserThreads;
		if (AppiusConfig.getConfigBoolOrFalse ("dumpUserThreads")) {
			System.out
			.println ("\n\n *** Dumping All User Threads *** \n\n");
			dumpUserThreads = true;
		} else {
			dumpUserThreads = false;
		}
		for (final AppiusClaudiusCaecus listener : AppiusClaudiusCaecus.serverThreads
				.values ()) {
			try {
				listener.tick (t, deltaT);
				if (dumpUserThreads) {
					System.out.println (listener.toString ());
					System.out.println ("\n\n");
					System.out.println (AppiusClaudiusCaecus
							.stringify (listener.getStackTrace ()));
					System.out.println ("\n\n");
				}
			} catch (final UserDeadException e) {
				listener
				.sendAdminDisconnect (
						"Your Internet connection was believed to be disconnected. If you are seeing this message, please contact Customer Service. Code Lazarus "
						+ AppiusClaudiusCaecus
						.getRev (),
						"Disconnected from server", "Lazarus",
				"doa");
			} catch (final Throwable e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}
		for (final AcceptsMetronomeTicks listener : AppiusClaudiusCaecus.metronomeListeners) {
			try {
				if (null == listener) {
					AppiusClaudiusCaecus.metronomeListeners
					.remove (listener);
				} else {
					listener.tick (t, deltaT);
				}
			} catch (final UserDeadException e) {
				// No op
			} catch (final Throwable e) {
				AppiusClaudiusCaecus.reportBug (e);
			}
		}

		if (t - AppiusClaudiusCaecus.tGameStatePump > AppiusConfig
				.getIntOrDefault (
						"org.starhope.appius.gameStateChangeTime", 400)) {
			GameEvent.propagateGameStateChange ();
			AppiusClaudiusCaecus.tGameStatePump = t;
		}
		if (t - AppiusClaudiusCaecus.tStats > AppiusConfig
				.getIntOrDefault ("org.starhope.appius.statsTimer",
						21600000)) {
			final int sum = AppiusClaudiusCaecus
			.getAccurateHeadcount ();
			AppiusClaudiusCaecus.bugDuplex (
					"Head Count — Appius Claudius Caecus",
					"The current number of users online is "
					+ sum
					+ "; with a high water mark of "
					+ AppiusClaudiusCaecus.getHighWaterUsers ()
					+ ". Server started at "
					+ new java.sql.Timestamp (
							AppiusClaudiusCaecus.bootTime)
					.toString ());
			AppiusClaudiusCaecus.tStats = t;
		}
		AppiusClaudiusCaecus.lastMetronomeTick = t;

	}

	/**
	 * Force a stack backtrace without an exception being thrown. For
	 * debugging purposes, principally.
	 */
	public static void traceThis () {
		AppiusClaudiusCaecus.traceThis ("Trace This");
	}

	/**
	 * Force a stack backtrace without an exception being thrown. For
	 * debugging purposes, principally.
	 * 
	 * @param string A string to be used as a label on the trace
	 */
	public static void traceThis (final String string) {
		final Throwable t = new Throwable ();
		try {
			throw t;
		} catch (final Throwable blah) {
			AppiusClaudiusCaecus.bugDuplex (string
					+ " — Appius Claudius Caecus", string
					+ " forcing stack backtrace"
					+ AppiusClaudiusCaecus.stringify (blah));
		}
	}

	/**
	 * Update the high water mark, if necessary
	 * 
	 */
	public static void updateHighWaterMark () {
		final int currentUsers = AppiusClaudiusCaecus
		.getAccurateHeadcount ();
		if (currentUsers > AppiusClaudiusCaecus.highWaterUsers) {
			AppiusClaudiusCaecus.highWaterUsers = currentUsers;
		}
	}

	/**
	 * While we are processing a transaction for the user (during
	 * command processing), this flag is brought high to block idle
	 * timeouts due to overlong transactions
	 */
	private boolean busyState;

	/**
	 * The language variant that the client is speaking.
	 */
	private float clientProtocolLanguage = 0;

	/**
	 * Boolean flag indicating whether the server is in debugging mode
	 * or not. True if we are in debugging mode. Defaults to true, until
	 * the configuration has been read
	 */
	private boolean debug = true;

	/**
	 * The queue of all datagrams pending for this user. The user should
	 * receive these as soon as possible.
	 */
	private final ConcurrentLinkedQueue <AppiusDatagram> futureDatagrams = new ConcurrentLinkedQueue <AppiusDatagram> ();

	/**
	 * At what time was the user warned about being idle for too long?
	 */
	private long idleWarned = 0;

	/**
	 * The buffered input stream from the user.
	 */
	private BufferedReader in = null;

	/**
	 * This indicates whether the thread has voluntarily decided to
	 * exit, e.g. because the user has properly disconnected and so
	 * forth.
	 */
	private boolean isDone = false;

	/**
	 * This variable controls the main loop of the server thread. When
	 * it transitions to “false,” the thread will die.
	 */
	private boolean keepRunning = true;

	/**
	 * The time at which we last received input from the remote user.
	 */
	private long lastInputTime = -1;

	/**
	 * This is a crazy XML looking string that we have to pump out to
	 * make the Flash plug-in happy.
	 */
	private final String letsPlayWithFlash;

	/**
	 * If the user has been logged in, this flag will be true
	 */
	private boolean loggedIn = false;

	/**
	 * The maximum number of characters (or is it bytes? I'm unclear on
	 * my own implementation there!) that can be accepted from the
	 * client in a single packet
	 */
	private final int maxInputSize;

	/**
	 * The user account that is logged in on this thread
	 */
	private User myUser;

	/**
	 * The output stream connected to the client
	 */
	PrintWriter out = null;

	/**
	 * The number of prelogin commands that can be accepted before the
	 * user is dropped for failing to log in
	 */
	private int preloginCountdown = 10;

	/**
	 * random key used for SHA1 sum in login
	 */
	private transient String randomKey;

	/**
	 * The socket connected to the client session
	 */
	private Socket socket;

	/**
	 * The state of the conversation that we are having with the client
	 */
	private int state = ProtocolState.WAITING;

	/**
	 * Time at which users were last nudged to check their online status
	 */
	private long tLastNudge = 0;

	/**
	 * The Zone in which this thread is connecting
	 */
	private Zone zone;

	/**
	 * Create a new thread connected to a given client, in a certain
	 * zone.
	 * 
	 * @param newSocket The socket connected to the client
	 * @param newZone The login zone into which the client is connected
	 */
	public AppiusClaudiusCaecus (final Socket newSocket,
			final Zone newZone) {
		setPriority (Thread.NORM_PRIORITY);
		setName ("prelogin @"
				+ newSocket.getRemoteSocketAddress ().toString ());
		AppiusClaudiusCaecus.startTicking (this);
		letsPlayWithFlash = "<?xml version=\"1.0\"?>\n"
			+ "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
			+ "<cross-domain-policy>\n"
			+ "<!-- Place top level domain name -->\n"
			+ "<allow-access-from domain=\""
			+ AppiusConfig.getTLD ()
			+ "\" secure=\"false\"/>\n"
			+ "<allow-access-from domain=\"tootsville.com\" to-ports=\"80,443,"
			+ AppiusClaudiusCaecus.getServerPort ()
			+ "\"/>\n"
			+ "<allow-http-request-headers-from domain=\"tootsville.com\" headers=\"*\" />\n"
			+ "<!-- use if you need access from subdomains. testing/www/staging.domain.com -->\n"
			+ "<allow-access-from domain=\"*."
			+ AppiusConfig.getTLD ()
			+ "\" secure=\"false\" />\n"
			+ "<allow-access-from domain=\"*."
			+ AppiusConfig.getTLD ()
			+ "\" to-ports=\"80,443,"
			+ AppiusClaudiusCaecus.getServerPort ()
			+ "\" />\n"
			+ "<allow-http-request-headers-from domain=\"*.tootsville.com\" headers=\"*\" />\n"
			+ "<allow-access-from domain=\"*\" secure=\"false\" />\n"
			+ "<allow-access-from domain=\"*\" to-ports=\"80,443,"
			+ AppiusClaudiusCaecus.getServerPort ()
			+ "\" />\n"
			+ "<allow-http-request-headers-from domain=\"*\" headers=\"*\" />\n"
			+ "</cross-domain-policy>\n";
		maxInputSize = AppiusConfig.getMaxInputSize ();
		socket = newSocket;
		zone = newZone;
		genRandomKey ();
		tattle ("> New connection.", true);
		tattle ("> Set random key:\t" + randomKey);
	}

	/**
	 * Just for uncaught exception handler faux-thread
	 * 
	 * @param string thread name
	 */
	public AppiusClaudiusCaecus (final String string) {
		super (string);
		randomKey = getRandomKey ();
		letsPlayWithFlash = "";
		maxInputSize = 1;
	}

	/**
	 * Send a packet to the user to see if they're still there
	 * 
	 * @throws UserDeadException if the user is disconnected
	 */
	private void areYouThere () throws UserDeadException {
		if (sendFutureDatagrams ()) throw new UserDeadException ();
		final long t = System.currentTimeMillis ();
		if (t - tLastNudge < AppiusConfig.getNudgeTime ()) return;
		tLastNudge = t;
		if (null != myUser) {
			/* Logged-in user thread */
			final JSONObject areYouThere = new JSONObject ();
			try {
				areYouThere.put ("from", "ayt");
				sendResponse (areYouThere, myUser.getRoomNumber (),
						false);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"unsolicited pong packet could not be constructed",
						e);
			}
		}
	}

	/**
	 * Close the socket, terminate the connection
	 */
	public void close () {
		tattle ("/*/ Closing connection");

		dropSocketConnection ();
		AppiusClaudiusCaecus.stopTicking (this);
		setLoggedIn (false);
		isDone = true;
		state = ProtocolState.PRELOGIN;

		AbstractRoom userRoom = null;
		try {
			if (null == myUser) return;
			synchronized (myUser) {
				userRoom = myUser.getRoom ();
			}
		} catch (final NullPointerException e) {
			return;
		}
		try {
			if (null != userRoom) {
				tattle ("/*/ Parting from room "
						+ userRoom.getMoniker () + " (#"
						+ userRoom.getID () + ")");
				userRoom.part (myUser);
			}
		} catch (final NullPointerException e) {
			// No op
		}

		try {
			if (null != zone) {
				synchronized (zone) {
					tattle ("/*/ Parting from zone " + zone.getName ());
					zone.remove (myUser);
				}
			}
		} catch (final NullPointerException e) {
			// No op
		} finally {
			zone = null;
		}

		try {
			if (null != myUser) {
				synchronized (myUser) {
					tattle ("/*/ Removing user from live caches");
					myUser.invalidateCache ();
				}
			}
		} catch (final NullPointerException e) {
			// No op
		} finally {
			myUser = null;
		}

	}

	/**
	 * Process a command from a JSON source
	 * 
	 * @param cmd The command to be processed
	 * @param jso The JSON data object to be passed into the relevant
	 *            command
	 * @param klass The dispatcher class responsible for handling this
	 *            command
	 */
	public void commandJSON (final String cmd, final JSONObject jso,
			final Class <?> klass) {
		LibMisc.commandJSON (cmd, jso, this, myUser, klass);
	}

	/**
	 * Compare two {@link AppiusClaudiusCaecus} server threads
	 * 
	 * @param other another server thread
	 * @return true, if the threads are the same
	 * 
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo (final AppiusClaudiusCaecus other) {
		return getName ().compareTo (other.getName ());
	}

	/**
	 * Disconnect *this* user, with a notification that they have logged
	 * in from someplace else.
	 */
	public void disconnectDuplicate () {
		if (null != zone && null != myUser) {
			AppiusClaudiusCaecus.logEvent ("login.duplicate", zone
					.getName (), myUser.getUserName (), myUser
					.getPassword (), null);
			sendAdminDisconnect (LibMisc.getText ("login.duplicate"),
					"Duplicate Login", "System", "login.duplicate");
		}
	}

	/**
	 * Drop the I/O socket for this user.
	 */
	private synchronized void dropSocketConnection () {
		tattle ("/*/ Closing I/O socket");
		if ( !Thread.currentThread ().getName ().equals (getName ())
				&& !Thread.currentThread ().getName ().equals (
				"metronome")) {
			AppiusClaudiusCaecus
			.traceThis ("called dropSocketConnection on a different thread: caller is "
					+ Thread.currentThread ().toString ()
					+ " and victim is " + toString ());
		}
		AppiusClaudiusCaecus.charon.addZombie (this);
		if (null != out) {
			synchronized (out) {
				out.close ();
			}
			out = null;
		}
		if (null != socket) {
			try {
				LibMisc.shutdownInput (socket);
			} catch (final IOException e) {
				/*
				 * Ignore. Seems to mostly mean that the socket were
				 * closed anyways.
				 */
			}
		}
		if (null != in) {
			synchronized (in) {
				try {
					in.close ();
				} catch (final IOException e) {
					// yay
				}
			}
			in = null;
		}
		if (null != socket) {
			synchronized (socket) {
				if ( !socket.isClosed ()) {
					try {
						try {
							socket.shutdownInput ();
						} catch (final IOException e) {
							// yay
						}
						try {
							socket.shutdownOutput ();
						} catch (final IOException e) {
							// yay
						}
						try {
							socket.close ();
						} catch (final IOException e) {
							// yay
						}
					} catch (final Exception e) {
						AppiusClaudiusCaecus
						.reportBug (
								"Unexpected exception while disconnecting user",
								e);
					}
				}
			}
			socket = null;
		}
		return;
	}

	/**
	 * Indicate that this thread should cease to breathe. This sets the
	 * keepRunning flag to “false” to terminate the main loop, and
	 * throws an arbitrary interrupt to smash whatever might be going on
	 * already.
	 */
	private void end () {
		tattle ("Good-bye, cruel worlds!");
		keepRunning = false;
		interrupt ();
	}

	/**
	 * Enter into a Zone (set the local zone indicator)
	 * 
	 * @param zoneName the name of the Zone
	 */
	public void enterZone (final String zoneName) {
		zone = AppiusClaudiusCaecus.getZone (zoneName);
	}

	/**
	 * Enter into a Zone (set the local zone indicator)
	 * 
	 * @param whichZone The zone being entered
	 */
	public void enterZone (final Zone whichZone) {
		zone = whichZone;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @param other the other thread against which to compare
	 * @return true, if these are the same thread
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	public boolean equals (final AppiusClaudiusCaecus other) {
		if (null == other) return false;
		return getName ().equals (other.getName ());
	}

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

	/**
	 * This method is called when login fails. At present, it just
	 * closes the connection.
	 */
	public void failLogin () {
		close ();
	}

	/**
	 * Generate a new random key, avoiding characters that won't work
	 * with Smart Fox Server clients
	 */
	private void genRandomKey () {
		final StringBuilder randy = new StringBuilder ();
		boolean truncate = false;
		debug = AppiusConfig
		.getConfigBoolOrFalse ("org.starhope.appius.debug");
		for (int i = 0; i < 20 && (i > 10 ? truncate == false : true); ++i) {
			char tryChar = '\0';
			while (tryChar <= ' ' || tryChar == '&' || tryChar == '<'
				|| tryChar == '\'') {
				tryChar = (char) AppiusConfig.getRandomInt (33, 126);
			}
			randy.append (tryChar);
			truncate = AppiusConfig.getRandomInt (1, 10) > 7;
		}
		randomKey = randy.toString ();
	}

	/**
	 * Get the “apple” (CHAP authentication string SHA1 digest encoded
	 * in hex) for the login system
	 * 
	 * @param pass The plaintext password to be used
	 * @return the “apple” string
	 */
	public String getApple (final String pass) {
		/* Check the CHAP apple hex code sequence */
		final String apple = getRandomKey ();

		byte [] sha1digest;
		try {
			final MessageDigest stomach = MessageDigest
			.getInstance ("SHA1");
			stomach.reset ();
			try {
				stomach.update ( (apple + pass).getBytes ("US-ASCII"));
			} catch (final UnsupportedEncodingException e) {
				AppiusClaudiusCaecus.blather ("can't go 16 bit");
			}
			sha1digest = stomach.digest ();
		} catch (final NoSuchAlgorithmException e) {
			throw AppiusClaudiusCaecus.fatalBug (new Exception (
			"Can't understand how to do SHA1 digests"));
		} catch (final NumberFormatException e) {
			throw AppiusClaudiusCaecus
			.fatalBug (new Exception (
			"Can't do some magic to make the SHA1 digest for login"));
		}
		final StringBuilder sha1hex = new StringBuilder (
				sha1digest.length * 2);
		for (final byte element : sha1digest) {
			sha1hex.append (Integer.toString (
					(element & 0xff) + 0x100, 16).substring (1));
		}
		return sha1hex.toString ();
	}

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

	/**
	 * @return The string form of the client's IP address (may be IPv4
	 *         or IPv6)
	 */
	public String getIPAddress () {
		if (null == socket) return null;
		final String hostAndAddress = socket.getInetAddress ()
		.toString ();
		final String [] chunky = hostAndAddress.split ("/");
		if (chunky.length < 2) return null;
		return chunky [1];
	}

	/**
	 * @return the random key sequence generated for this client thread
	 */
	public String getRandomKey () {
		return randomKey;
	}

	/**
	 * @return the sfVersion
	 */
	public float getSFSVersion () {
		// default getter (brpocock, Oct 14, 2009)
		return clientProtocolLanguage;
	}

	/**
	 * Get the socket connection to the client
	 * 
	 * @return the socket connection to the client
	 */
	private Socket getSocket () {
		return socket;
	}

	/**
	 * @return the user connected to this server thread
	 */
	public User getUser () {
		return myUser;
	}

	/**
	 * Get the zone in which this client is acting
	 * 
	 * @return the zone in which this client is acting
	 */
	public Zone getZone () {
		return zone;
	}

	/**
	 * Get input from the client stream
	 * 
	 * @return the input from the client
	 * @throws UserDeadException if the user disconnects
	 */
	private String grabInput () throws UserDeadException {
		final StringBuilder inputBuilder = new StringBuilder ();
		int chr = -1;
		while (keepRunning) {

			if (socket.isClosed ()) return null;

			if (sendFutureDatagrams ()) return null;

			/*
			 * Any input
			 */
			try {
				synchronized (in) {
					if (in.ready ()) {
						try {
							chr = in.read ();
						} catch (final java.net.SocketException e) {
							chr = -1;
							tattle ("//quit// User disconnected (or something like that)");
						}
						if (chr == '\0') {
							break;
						}
						if (chr == -1) {
							isDone = true;
							close ();
							break;
						}
						inputBuilder.append ((char) chr);
						if (inputBuilder.length () > maxInputSize) {
							tattle ("!Input exceeds " + maxInputSize
									+ " chars; cutting it off here.\n");
							break;
						}
					} else { // no input pending
						try {
							areYouThere ();
							Thread.yield ();
							Thread
							.sleep (AppiusConfig
									.getIntOrDefault (
											"org.starhope.appius.ioDelay",
											100));
						} catch (final InterruptedException e) {
							// no problem, keep going
						} catch (final UserDeadException e) {
							return null;
						}
					}
				}
			} catch (final IOException e) {
				AppiusClaudiusCaecus.reportBug (e);
				close ();
				return null;
			}

		}
		final String inputLine = inputBuilder.toString ();
		if (isDebug ()) {
			tattle ("//recv//" + inputLine);
		}

		return inputLine;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (getName ());
	}

	/**
	 * Determine whether the server is in debugging mode
	 * 
	 * @return true, if the server is in debug mode
	 */
	public boolean isDebug () {
		return debug;
	}

	/**
	 * @return true, if a user has logged in on this thread
	 */
	public boolean isLoggedIn () {
		return loggedIn;
	}

	/**
	 * Kick offline any duplicates of the given user as s/he logs in
	 * 
	 * @param user The user logging in (whose duplicates should be
	 *            disconnected)
	 * @param nick The user's nickname (login name)
	 * 
	 */
	protected void kickDuplicates (final User user, final String nick) {
		if (null == user) return;
		if (user.isOnline () && !this.equals (user.getServerThread ())) {
			tattle (nick
					+ " is now here in two zones. Let's get rid of that evil twin from "
					+ user.getLastZone () + "...");
			user.getServerThread ().disconnectDuplicate ();
		}
	}

	/**
	 * Kick offline any duplicates of the given user as s/he logs in
	 * 
	 * @param user The user logging in (whose duplicates should be
	 *            disconnected)
	 * @param nick The user's nickname (login name)
	 * @param password The user's password (ignored)
	 * 
	 * @deprecated use {@link #kickDuplicates(User, String)}
	 */
	@Deprecated
	protected void kickDuplicates (final User user, final String nick,
			final String password) {
		kickDuplicates (user, nick);
	}

	/**
	 * Process a login request from the user
	 * 
	 * @param z The zone into which the user is trying to log in
	 * @param bigNick The user's requested nickname (attempted user
	 *            name)
	 * @param password This is a bit of a misnomer. We actually are
	 *            checking for the secret key (CHAP cookie) for the
	 *            current channel, to which has been appended the user's
	 *            actual password, as presented as a hex-coded SHA1
	 *            digest. (In brief: pseudocode of sha1( cookie +
	 *            password ).toHex )
	 * @return true, if the user can log in; false, if they were refused
	 */
	public boolean logIn (final Zone z, final String bigNick,
			final String password) {
		final String nick = bigNick.toLowerCase (Locale.ENGLISH);
		AppiusClaudiusCaecus.blather ("attempt as “" + nick + "”", "",
				socket.getRemoteSocketAddress ().toString (),
				"Login attempt with “" + password + "”", true);

		// z.checkZonesForSpawn ();

		final String zoneName = z.getName ();

		/* Figure out who will receive a call back, if we have to */
		final LinkedList <AppiusClaudiusCaecus> recipients = new LinkedList <AppiusClaudiusCaecus> ();
		recipients.add (this);

		if (nick.startsWith ("$loadclient.")) {
			AbstractUser load = User.getByLogin (nick);
			if (null == load) {
				try {
					load = User
					.create (
							new Date (
									System.currentTimeMillis ()
									- AppiusClaudiusCaecus.TWENTY_ONE_YEARS_MSEC),
									"moo", nick);
				} catch (final NumberFormatException e) {
					AppiusClaudiusCaecus
					.reportBug (
							"Load client autocreation hit an irrational date of birth",
							e);
					return false;
				} catch (final AlreadyUsedException e) {
					load = User.getByLogin (nick);
				} catch (final ForbiddenUserException e) {
					AppiusClaudiusCaecus
					.reportBug (
							"Caught a ForbiddenUserException for load client login",
							e);
					return false;
				}

			}
			if (load instanceof User) {
				((User) load).setPassword ("p4$$w0rd");
			}
			load.setAgeGroupToSystem ();
		}

		/* See if there's a user with the specified nickname */
		final AbstractUser user = User.getByLogin (nick);
		if (null == user) {
			z.sendNoSuchUser (recipients, nick, zoneName, password);
			return false;
		}

		if ( ! (user instanceof User)) {
			z.sendNoSuchUser (recipients, nick, zoneName, password);
		}

		/* Check the password */
		if ( !password.equals (getApple ( ((Person) user)
				.getPassword ()))) {
			z.sendBadPassword (nick, this, (User) user, zoneName,
					password);
			return false;
		}

		if (user.isCanceled ()) {
			sendLogKO (LibMisc.getText ("canceled"));
			return false;
		}

		if (user.isBanned () || user.isKicked ()) {
			sendLogKO (user.getKickedMessage ());
			return false;
		}

		if (AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.betaTest")) {
			if (! ((User) user).canBetaTest ()) {
				sendLogKO ("This is a private server. Your account is not enabled here.");
				return false;
			}
		}

		/* At this point, they are actually logging in */
		((User) user).updateCache ();
		Thread.currentThread ().setName (
				"u:"
				+ user.getAvatarLabel ()
				+ "=#"
				+ user.getUserID ()
				+ "@"
				+ getSocket ().getRemoteSocketAddress ()
				.toString ());

		/* Check for duplicate user in any Zone */
		kickDuplicates ((User) user, password);

		AppiusClaudiusCaecus.blather (nick, "Zone “" + zoneName + "”",
				socket.getRemoteSocketAddress ().toString (),
				"Welcome", false);
		myUser = (User) user;
		setLoggedIn (true);
		((User) user).setServerThread (this);
		state = ProtocolState.READY;
		z.add (user);

		sendLoginPacket (zoneName, nick, password);
		((User) user).postLoginGlobal ();

		return true;
	}

	/**
	 * Order this user to migrate to another Appius Claudius Caecus
	 * server.
	 * 
	 * @param hostName The alternate server's public host name or IP
	 *            address string
	 * @param portNumber The listening port on the alternate server
	 * @param zoneName The zone name to which the user should connect
	 */
	public void migrate (final String hostName, final int portNumber,
			final String zoneName) {
		try {
			sendRawMessage ("{from:\"migrate\", status:true, host:\""
					+ hostName + "\", port:\"" + portNumber
					+ "\", zone:\"" + zoneName + "\"}", true);
		} catch (final UserDeadException e) {
			// Dead users don't have to migrate.
		}
	}

	/**
	 * Process and dispatch input from the client.
	 * 
	 * @param theInput The input packet from the client
	 * @return An output packet for the client, or a zero-length string
	 *         if there is no input; or, a null pointer if the client
	 *         should be disconnected.
	 */
	public String processInput (final String theInput) {

		try {
			if (isDone)
				return null;
			else if (0 == theInput.length ()) {
				if (state == ProtocolState.WAITING) {
					tattle ("Entered via WAITING state through nothingness");
					state = ProtocolState.PRELOGIN;
					return letsPlayWithFlash;
				}
				tattle ("No input. Not going to process the emptiness");
				return "";
			}
		} catch (final Throwable e) {
			return "?ERR\tE\t[[[" + AppiusClaudiusCaecus.stringify (e)
			+ "]]]\07\n\0";
		}

		lastInputTime = System.currentTimeMillis ();

		/* Cubist? */
		if (Float.POSITIVE_INFINITY == clientProtocolLanguage) {
			switch (state) {
			case ProtocolState.CLOSED:
			default:
				sendAdminDisconnect (
						"You were disconnected from the server successfully",
						"Disconnect", "System", "protocol.closed");
				return null;
			case ProtocolState.PRELOGIN:
				return processJSONLogin (theInput);
			case ProtocolState.READY:
				return processJSONInput (theInput);
			}
		}

		if (theInput.equals ("∞")
				|| theInput.equals ("Infinity, please")) {
			setSFSVersion (Float.POSITIVE_INFINITY);
			return "∞\tYou're quite welcome.\t0\n";
		}

		/* Smart Fox Server Protocol */
		switch (state) {
		case ProtocolState.CLOSED:
		default:
			sendAdminDisconnect (
					"You were disconnected from the server successfully",
					"Disconnect", "System", "protocol.closed");
			return null;
		case ProtocolState.WAITING:
			tattle ("Entered via WAITING state");
			state = ProtocolState.PRELOGIN;
			return letsPlayWithFlash;
		case ProtocolState.PRELOGIN:
			try {
				return processPreLogin (theInput);
			} catch (final UserDeadException e) {
				return null;
			}
		case ProtocolState.READY:
			if (theInput.charAt (0) == '<')
				return processXMLInput (theInput);
			else if (theInput.charAt (0) == '{')
				return processJSONInput (theInput);
			else return "?ERR\tformat\07\n";
		}
	}

	/**
	 * Process a JSON string from the client
	 * 
	 * @param theInput The JSON string containing the command and data
	 * @return A sequence to return to the client
	 */
	private String processJSONInput (final String theInput) {
		JSONObject jso = null;
		try {
			jso = new JSONObject (theInput);
			final JSONObject callParams;
			if (jso.has ("o")) {
				callParams = jso.getJSONObject ("o");
				final int roomNumber = jso.getInt ("r");
				final String cmd = jso.getString ("xt");
				zone
				.handleRequest (cmd, callParams, myUser,
						roomNumber);
			} else {
				final JSONObject bParams = jso.getJSONObject ("b");
				final String cmd = bParams.getString ("c");
				final int roomNumber = bParams.getInt ("r");
				callParams = bParams.getJSONObject ("p");
				zone
				.handleRequest (cmd, callParams, myUser,
						roomNumber);
			}
			return "";
		} catch (final JSONException e) {
			return "?ERR\tjson\t" + e.toString ();
		}
	}

	/**
	 * Process a JSON command sequence from the client
	 * 
	 * @param theInput the JSON data in string form
	 * @return output for the client
	 */
	private String processJSONLogin (final String theInput) {
		JSONObject jso = null;
		try {
			jso = new JSONObject (theInput);
			if (jso.has ("get")) {
				if (jso.getString ("get").equals ("key"))
					return "{from:\"loginKey\", success:true, key:\""
					+ getRandomKey () + "\"}";
			} else if (jso.has ("login")) {
				final String zoneName = jso.getString ("zone");
				final String userName = jso.getString ("login");
				final String password = jso.getString ("key");
				tattle ("login attempt\tzone:" + zoneName + "\tlogin:"
						+ userName + "\tpass:" + password);
				if (zoneName.length () > 0 && userName.length () > 0
						&& password.length () > 0) {
					enterZone (zoneName);
					if (null == zone
							|| !zone.getName ().equals (zoneName)) {
						tattle ("didn't find the zone");
						return "?ERR\tzone\07\n";
					}
					tattle ("handling login process");
					if (logIn (zone, userName, password)) {
						state = ProtocolState.READY;
					}
				}
				return "";
			}
		} catch (final JSONException e) {
			return "?ERR\tjson\t" + e.toString ();
		}
		return "?ERR\tprelogin.syntax\07\n";
	}

	/**
	 * Process a prelogin JSON command
	 * 
	 * @param theInput The input string, which must contain a
	 *            properly-formatted JSON command sequence
	 * @return a result string to be returned to the client
	 */
	private String processJSONPreLogin (final String theInput) {
		JSONObject jso = null;
		try {
			jso = new JSONObject (theInput);

			if (jso.has ("c")) {
				final JSONObject callParams = jso.getJSONObject ("d");

				commandJSON (jso.getString ("c"), callParams,
						PreLoginCommands.class);
			} else return "?ERR\tjson\tno c\n";
		} catch (final JSONException e) {
			return "?ERR\tjson\t" + e.toString ();
		}
		return "";
	}

	/**
	 * Process a prelogin input sequence
	 * 
	 * @param theInput the prelogin input sequence
	 * @return the output string to return to the client
	 * @throws UserDeadException if the user disconnects
	 */
	private String processPreLogin (final String theInput)
	throws UserDeadException {
		if ( --preloginCountdown < 0) {
			sendRawMessage ("?ERR\tnolog\07\n", false);
			sendAdminDisconnect (
					"Failed to present login credentials: You did not provide a user name and password within the allowed limits",
					"Failed to log in", "System", "protocol.nolog");
			return null;
		}

		if (theInput.charAt (0) == '{')
			return processJSONPreLogin (theInput);

		if (theInput.indexOf ("policy-file-request") > -1)
			return letsPlayWithFlash;

		if (theInput.indexOf ("rndK") > -1)
			return "<msg t='sys'><body action='rndK' r='-1'><k>"
			+ getRandomKey () + "</k></body></msg>";

		if (theInput.indexOf ("verChk") > -1) {
			if (theInput.indexOf ("158") > -1) {
				setSFSVersion ((float) 1.58);
			} else if (theInput.indexOf ("\u221e") > -1) {
				setSFSVersion (Float.POSITIVE_INFINITY);
			} else return "?ERR\tverChk\07\n";
			return "<msg t='sys'><body action='apiOK' r='0'></body></msg>";
		}

		if (theInput.indexOf ("login") > -1) {
			final String [] segments = theInput.split ("'");
			if (segments.length < 7) return "?ERR\tlogin.syntax\07\n";
			final String zoneName = segments [7];
			final String userName = theInput.substring (theInput
					.indexOf ("<nick><![CDATA[") + 15, theInput
					.indexOf ("]]>"));
			String password = theInput.substring (theInput
					.indexOf ("<pword><![CDATA[") + 16);
			password = password.substring (0, password.indexOf ("]]>"));
			tattle ("login attempt\tzone:" + zoneName + "\tlogin:"
					+ userName + "\tpass:" + password);
			if (zoneName.length () > 0 && userName.length () > 0
					&& password.length () > 0) {
				enterZone (zoneName);
				if (null == zone || !zone.getName ().equals (zoneName)) {
					tattle ("didn't find the zone");
					return "?ERR\tzone\07\n";
				}
				tattle ("handling login process");
				if (logIn (zone, userName, password)) {
					state = ProtocolState.READY;
				}
			}
			return "";
		}

		return "?ERR\tprelogin\tsyntax\07\n";
	}

	/**
	 * @param theInput The input stream, expected to be in Smart Fox
	 *            Server Pro XML format
	 * @return A result string to be returned to the user
	 */
	private String processXMLInput (final String theInput) {
		tattle ("XML input: " + theInput);
		final Document xml;
		try {
			final DocumentBuilderFactory dbf = DocumentBuilderFactory
			.newInstance ();

			try {

				// Using factory get an instance of document builder
				final DocumentBuilder db = dbf.newDocumentBuilder ();

				// parse using builder to get DOM representation of the
				// XML file
				xml = db.parse (new ByteArrayInputStream (theInput
						.getBytes ()));

			} catch (final ParserConfigurationException pce) {
				AppiusClaudiusCaecus.reportBug (pce);
				return "?ERR\txml\tconfig\07\n";
			} catch (final SAXException se) {
				AppiusClaudiusCaecus.reportBug (se);
				return "?ERR\txml\tsax\07\n";
			} catch (final IOException ioe) {
				AppiusClaudiusCaecus.reportBug (ioe);
				return "?ERR\txml\tio\07\n";
			}

		} catch (final java.lang.ExceptionInInitializerError e) {
			return "?ERR\txml\tcan't initialize XMLParser\07\n";
		}
		final Node msgEl = xml.getChildNodes ().item (0);
		if (null == msgEl) return "?ERR\txml\tno top element\07\n";
		if ( !msgEl.getNodeName ().equals ("msg"))
			return "?ERR\txml\texpected msg element\tgot\t"
			+ msgEl.getNodeName () + "\07\n";
		final Node tAttr = msgEl.getAttributes ().getNamedItem ("t");
		if (null == tAttr)
			return "?ERR\txml\texpected t attribute\07\n";
		if ( !tAttr.getNodeValue ().equals ("sys"))
			return "?ERR\txml\texpected t=sys\07\n";
		final Node body = msgEl.getChildNodes ().item (0);
		if (null == body)
			return "?ERR\txml\texpected body element\07\n";
		final Node actionAttr = body.getAttributes ().getNamedItem (
		"action");
		if (null == actionAttr)
			return "?ERR\txml\texpected action attribute\07\n";
		final String cmd = actionAttr.getNodeValue ();
		final Node rAttr = body.getAttributes ().getNamedItem ("r");
		if (null == rAttr)
			return "?ERR\txml\texpected room attribute\07\n";
		final Integer roomNum = Integer
		.parseInt (rAttr.getNodeValue ());
		final NodeList data = body.getChildNodes ();
		tattle ("XML (SmartFaux) command\t" + cmd);
		if (cmd.equals ("getRmList"))
			return getZone ().getRoomListSFSXML ();
		else if (cmd.equals ("logout")) {
			state = ProtocolState.PRELOGIN;
			// close ();
			return "?Bye!";
		} else if (cmd.equals ("setRvars")) {
			final NodeList vars = data.item (0).getChildNodes ();
			for (int i = 0; i < vars.getLength (); ++i) {
				final Node var = vars.item (i);
				getZone ().getRoom (roomNum).setVariable (
						var.getAttributes ().getNamedItem ("n")
						.getNodeValue (),
						var.getFirstChild ().getNodeValue ());
			}
			return "";
		} else if (cmd.equals ("setUvars")) {
			final NodeList vars = data.item (0).getChildNodes ();
			for (int i = 0; i < vars.getLength (); ++i) {
				final Node var = vars.item (i);
				myUser.setVariable (var.getAttributes ().getNamedItem (
				"n").getNodeValue (), var.getFirstChild ()
				.getNodeValue ());
			}
			return "";
		} else if (cmd.equals ("joinRoom")) {
			final Node room = data.item (0);
			if ( !room.getNodeName ().equals ("room"))
				return "?ERR\tjoinRoom\texpected room\07\n";
			final Node idAttr = room.getAttributes ().getNamedItem (
			"id");
			if (null == idAttr)
				return "?ERR\tjoinRoom\texpected room ID\07\n";
			final int roomToJoin = Integer.parseInt (idAttr
					.getNodeValue ());
			// return zone.handleJoin (myUser, roomNum, roomToJoin);
			zone.getRoom (roomToJoin).join (myUser);
			return "";
		} else if (cmd.equals ("pubMsg")) {
			final String txt = data.item (0).getChildNodes ().item (0)
			.getNodeValue ();
			tattle ("Speech: “" + txt + "” from "
					+ data.item (0).getNodeName ());
			zone.handleSpeak (roomNum, myUser, txt);
			return "";
		} else return "?ERR\tcmd?\07\n\0";
	}

	/**
	 * Run the server thread connected to a client
	 * 
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run () {

		/*
		 * Setup stanza
		 */

		keepRunning = true;
		lastInputTime = System.currentTimeMillis ();
		AppiusClaudiusCaecus.charon.addZombie (this);
		AppiusClaudiusCaecus.updateHighWaterMark ();

		try {
			setup ();

			/*
			 * Main I/O loop
			 */

			while (keepRunning) {

				sendDeferredDatagrams ();

				/*
				 * Process input line
				 */
				if (null == socket || socket.isClosed ()) {
					close ();
					return; // End of thread.
				}

				final String inputLine = grabInput ();
				final String outputLine = processInput (inputLine);

				if (null == socket || socket.isClosed ()) {
					close ();
					return;
				}

				/*
				 * If the output is null pointer, we're done
				 */
				if (null == outputLine) {
					if (isDebug ()) {
						tattle ("//end.// The End.");
					}
					dropSocketConnection ();
					AppiusClaudiusCaecus.getCharon ().addZombie (this);
					return; // End of thread.
				}
				/*
				 * If the output is empty, don't send anything.
				 */
				if ( ! ("".equals (outputLine) || "\0"
						.equals (outputLine))) {
					sendRawMessage (outputLine, false);
				}
				if (isDone) {
					tattle ("//end.// Ending from EOF");
					close ();
					return; // End of thread.
				}

			}

		} catch (final IOException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final UserDeadException e) {
			close ();
			return;
		} catch (final Throwable e) {
			AppiusClaudiusCaecus.reportBug (e);
		} finally {
			dropSocketConnection ();
		}

		AppiusClaudiusCaecus.getCharon ().addZombie (this);

		// End of thread.
	}

	/**
	 * Send a disconnection message, and drop the user on the next
	 * client cycle. This is an asynchronous drop, but it clears all
	 * other pending output, ensuring that the client will receive this
	 * message next in queue and then disconnect.
	 * 
	 * @param message User-visible message explaining the disconnection
	 * @param title Title to display in message box
	 * @param label Label to display in corner of message box
	 * @param disconnectCause Cause code to return to client application
	 *            giving general cause for disconnection; e.g. "kick" or
	 *            "ban" usually
	 */
	public void sendAdminDisconnect (final String message,
			final String title, final String label,
			final String disconnectCause) {

		final JSONObject messagePacket = new JSONObject ();
		try {
			messagePacket.put ("from", "admin");
			messagePacket.put ("status", "true");
			messagePacket.put ("title", title);
			messagePacket.put ("label", label);
			messagePacket.put ("message", message);
			messagePacket.put ("disconnect", "true");
			messagePacket.put ("cause", disconnectCause);
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
			close ();
			return;
		}
		futureDatagrams.clear ();
		futureDatagrams.add (new AppiusDatagram (messagePacket
				.toString (), true));
		return;
	}

	/**
	 * Send an administrative message to the user.
	 * 
	 * @param message administrative message to send
	 * @param remote if true, this is being written to another user
	 * @throws UserDeadException if the user has been disconnected
	 * @see #sendAdminMessage(String, String, String, boolean)
	 */
	public void sendAdminMessage (final String message,
			final boolean remote) throws UserDeadException {
		sendAdminMessage (message, "", "ADMIN", remote);
	}

	/**
	 * Send an administrative message. Attempts to use the JSON protocol
	 * for all clients now. If the data can't be successfully encoded
	 * into
	 * 
	 * @param message The actual message text
	 * @param title The title, which displays in the same font above the
	 *            message, but does not scroll
	 * @param hatLabel A short label which identifies the general source
	 *            of the message, for dialog box decoration
	 * @param remote Whether to send this message remotely (true =
	 *            deferred delivery) or immediately (false)
	 * @throws UserDeadException if the user isn't there to receive the
	 *             message
	 */
	public void sendAdminMessage (final String message,
			final String title, final String hatLabel,
			final boolean remote) throws UserDeadException {

		if ( !AppiusConfig
				.getConfigBoolOrFalse ("it.gotoandplay.sfs.xmlAdminMessages")) {
			try {
				final JSONObject messagePacket = new JSONObject ();
				messagePacket.put ("from", "admin");
				messagePacket.put ("status", "true");
				messagePacket.put ("title", title);
				messagePacket.put ("label", hatLabel);
				messagePacket.put ("message", message);
				sendResponse (messagePacket, -1, remote);
				return;
			} catch (final JSONException e) {
				if (clientProtocolLanguage == Float.POSITIVE_INFINITY) {
					AppiusClaudiusCaecus.fatalBug (e);
				}
				// else, fall through to the XML dmnMsg form
			}
		}
		int roomID = -1;
		int userID = -1;
		if (null != myUser) {
			roomID = myUser.getRoomNumber ();
			userID = myUser.getUserID ();
			if (myUser.isOnline () && null != myUser.getZone ()) {
				myUser.getZone ().tellEaves (myUser, myUser.getRoom (),
						"mod", message);
			}
		}
		sendRawMessage ("<msg t='sys'>" + "<body action='dmnMsg' r='"
				+ roomID + "'>" + "<user id='" + userID + "'/>"
				+ "<txt><![CDATA[" + message + "]]></txt>"
				+ "</body></msg>", remote);
	}

	/**
	 * Send all deferred (future) datagrams pending in the queue
	 * 
	 * @throws UserDeadException if the user has disconnected
	 * 
	 */
	private void sendDeferredDatagrams () throws UserDeadException {
		/*
		 * All “remote” output enqueued
		 */
		if (futureDatagrams.size () > 0) {
			tattle ("// sending " + futureDatagrams.size ()
					+ " deferred datagrams //");
			while (futureDatagrams.size () > 0) {
				final AppiusDatagram gram = futureDatagrams.remove ();
				sendRawMessage (gram.getDatagram (), false);
				if (gram.isDisconnectAfterSending ()) {
					close ();
					isDone = true;
					throw new UserDeadException ();
				}
			}
		}
	}

	/**
	 * Send a raw error message back to the client as a JSON response
	 * 
	 * @param errorSource The method that is the source of the error
	 * @param message The error message to be returned
	 */
	public void sendError_RAW (final String errorSource,
			final String message) {
		final JSONObject result = new JSONObject ();
		try {
			tattle ("Sending error: from " + errorSource + "\n"
					+ message);
			result.put ("from", errorSource);
			result.put ("status", false);
			result.put ("err", message);
			sendResponse (result, -1);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 19, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final UserDeadException e) {
			// Ignored
		}
	}

	/**
	 * Send any future datagrams that are pending for this user
	 * 
	 * @return true, if the user has been disconnected
	 */
	private boolean sendFutureDatagrams () {
		/*
		 * All “remote” output enqueued
		 */
		if (futureDatagrams.size () > 0) {
			tattle ("// sending " + futureDatagrams.size ()
					+ " deferred datagrams //");
			while (futureDatagrams.size () > 0) {
				final AppiusDatagram gram = futureDatagrams.remove ();
				try {
					sendRawMessage (gram.getDatagram (), false);
				} catch (final UserDeadException e) {
					gram.setDisconnectAfterSending (true);
				}
				if (gram.isDisconnectAfterSending ()) {
					try {
						Thread
						.sleep (AppiusConfig
								.getIntOrDefault (
										"org.starhope.appius.futureDatagram.disconnectDelay",
										250));
					} catch (final InterruptedException e) {
						// Don't care.
					}
					close ();
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Send a game action event message to the client
	 * 
	 * @param sender The user sending the game action
	 * @param data Arbitrary data associated with the game action
	 * @throws JSONException if the data can't be represented as JSON
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendGameActionMessage (final AbstractUser sender,
			final JSONObject data)
	throws JSONException, UserDeadException {
		final JSONObject result = new JSONObject ();
		result.put ("fromUser", sender.getAvatarLabel ());
		result.put ("from", "gameAction");
		result.put ("status", "true");
		result.put ("data", data);
		sendResponse (result, myUser.getRoomNumber (), myUser);
	}

	/**
	 * Send the bucketfuls of information that we force-feed the client
	 * at login...
	 * 
	 * @param zoneName The name of the zone into which the user has
	 *            logged in
	 * @param nick The user's nickname
	 * @param password The user's password (SHA1 encoded with the local
	 *            random key)
	 */
	protected void sendLoginPacket (final String zoneName,
			final String nick, final String password) {
		/*
		 * Send login packet
		 */
		AppiusClaudiusCaecus.logEvent ("login", zoneName, nick,
				password, null);

		if (null == myUser) {
			AppiusClaudiusCaecus.reportBug ("Logged in no user?");
			sendLogKO ("An unexpected error has occurred and we are not able to identify the user account that you have named. "
					+ userDebug ("E001"));
			return;
		}
		myUser.loggedIn (zoneName, this);

		final JSONObject result = new JSONObject ();
		try {
			result.put ("_cmd", "logOK");
			/*
			 * In $Eden, send the list of “real” zones
			 */
			if ("$Eden".equals (zoneName)) {
				result.put ("zoneList", zone.getZoneList_JSON (myUser));
				result.put ("lastZone", myUser.getLastZone ());
			} else {
				AppiusClaudiusCaecus
				.blather ("Cast out from the garden…");
				result.put ("lobby", zone.getNextLobby ());
			}
			/*
			 * Other stuff we tell them about themselves.
			 */
			result.put ("user", myUser.toJSON ());
			result.put ("sfsUserID", myUser.getUserID ());
			AppiusClaudiusCaecus.blather ("LOGIN: " + result);
			try {
				sendResponse (result, -1, null);
			} catch (final UserDeadException e) {
				// Don't ask, don't care.
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
	}

	/**
	 * Send a login failure message to the client, using the default
	 * (generic) message
	 * 
	 */
	protected void sendLogKO () {
		sendLogKO (LibMisc.getText ("login.fail"));
	}

	/**
	 * Send a “KO” message to the client, informing them that they are
	 * not permitted to log in.
	 * 
	 * @param messageText The user-visible message given to the user
	 */
	protected void sendLogKO (final String messageText) {
		final JSONObject result = new JSONObject ();
		try {
			result.put ("_cmd", "logKO");
			result.put ("err", "login.fail");
			result.put ("msg", messageText);
			try {
				sendResponse (result, -1, false);
			} catch (final UserDeadException e) {
				// ironic
			}
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		}
		failLogin ();
	}

	/**
	 * Send a private (“whisper”) message to the user
	 * 
	 * @param from The user sending the message
	 * @param message The message being whispered
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendPrivateMessage (final AbstractUser from,
			final String message) throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject response = new JSONObject ();
			try {
				response.put ("from", "priv");
				response.put ("u", from.getUserID ());
				response.put ("t", message);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendResponse (response, from.getRoomNumber ());
			return;
		}
		sendRawMessage ("<msg t='sys'><body action='prvMsg' r='"
				+ from.getRoomNumber () + "'><user id='"
				+ from.getUserID () + "' /><txt><![CDATA[" + message
				+ "]]></txt></body></msg>", true);
	}

	/**
	 * Send a public message
	 * 
	 * @param from sender of the message (speaker)
	 * @param message The public message
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendPublicMessage (final AbstractUser from,
			final String message) throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject response = new JSONObject ();
			try {
				response.put ("from", "pub");
				response.put ("u", from.getUserID ());
				response.put ("t", message);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendResponse (response,
					(from instanceof User ? ((User) from)
							.getRoomNumber () : -1));
			return;
		}

		sendRawMessage ("<msg t='sys'><body action='pubMsg' r='"
				+ (from instanceof User ? ((User) from)
						.getRoomNumber () : -1) + "'><user id='"
						+ from.getUserID () + "' /><txt><![CDATA[" + message
						+ "]]></txt></body></msg>", true);
	}

	/**
	 * @param reply The string to be transmitted to another user
	 * @param remote True, if being written to another user
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRawMessage (final String reply, final boolean remote)
	throws UserDeadException {
		if (remote) {
			sendRawMessageLater (reply);
		} else {
			if (null == out) throw new UserDeadException ();
			synchronized (out) {
				tattle ("//SEND{" + futureDatagrams.size () + "}//"
						+ reply);
				out.print (reply);
				out.print ('\0');
				out.flush ();
				if (out.checkError ()) throw new UserDeadException ();
				/* Peek for input. Try to throw an I/O exception. */
				try {
					in.mark (1);
					final int ch = in.read ();
					if ( -1 == ch) throw new UserDeadException ();
					in.reset ();
				} catch (final IOException e) {
					throw new UserDeadException ();
				}
			}
		}
	}

	/**
	 * Send a message to the user in future
	 * 
	 * @param reply The message to be sent in future
	 * @throws UserDeadException if the user is already gone
	 */
	private void sendRawMessageLater (final String reply)
	throws UserDeadException {
		tattle ("//send//(" + futureDatagrams.size () + ")//" + reply);
		if (futureDatagrams.size () > AppiusConfig
				.getFutureDatagramsMax ()) {
			tattle ("User has too many future datagrams enqueued ("
					+ futureDatagrams.size ()
					+ " exceeds org.starhope.appius.futureDatagramsMax of "
					+ AppiusConfig.getFutureDatagramsMax ()
					+ ") and will be disconnected");
			sendAdminDisconnect (
					"Your connection to Tootsville™ has become backlogged due to a slow Internet connection. If you are running other programs that access the Internet, such as music or movie players or large downloads, you might need to stop them before signing in to Tootsville.",
					"Internet connection timeout", "System", "backlog");
			throw new UserDeadException ();
		}
		futureDatagrams.add (new AppiusDatagram (reply));
	}

	/**
	 * Send a response as a future (deferred remote) datagram without a
	 * room specified
	 * 
	 * @throws UserDeadException if the user disconnects
	 * @see #sendResponse(JSONObject, int, boolean)
	 * @see org.starhope.util.types.CanProcessCommands#sendResponse(org.json.JSONObject)
	 */
	public void sendResponse (final JSONObject result)
	throws UserDeadException {
		sendResponse (result, -1, true);
	}

	/**
	 * Send a response to the client in JSON form. In Smart Fox Server
	 * Pro compatibility mode, this goes through as a JSON Extension
	 * Response packet. In Cubist JSON form, the JSON object is returned
	 * “intact,” with the room number (if supplied as a positive number)
	 * added into it as the “r” key.
	 * 
	 * @param result the JSON object to be returned to the client
	 * @param room The room number from which the response is being
	 *            sent.
	 * @param remote Whether to send the message as a remote (deferred
	 *            future datagram) message
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendResponse (final JSONObject result, final int room,
			final boolean remote) throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			if (room > 0) {
				try {
					result.put ("r", room);
				} catch (final JSONException e) {
					AppiusClaudiusCaecus.fatalBug (e);
				}
			}
			sendRawMessage (result.toString (), remote);
			return;
		}

		sendRawMessage ("{\"t\":\"xt\",\"b\":{\"r\":" + room
				+ ",\"o\":" + result + "}}", remote);
	}

	/**
	 * Send a response as a future (deferred remote) datagram
	 * 
	 * @see #sendResponse(JSONObject, int, boolean)
	 * 
	 * @param result the JSON result object to be sent (“extension
	 *            response”)
	 * @param room The room in which the event occurred.
	 * @throws UserDeadException if the user has disconnected
	 */
	public void sendResponse (final JSONObject result,
			final Integer room) throws UserDeadException {
		sendResponse (result, room, true);
	}

	/**
	 * @param result the JSON result object to be sent (“extension
	 *            response”)
	 * @param room The room in which the event occurred.
	 * @param u The user to whom the message is being sent
	 * @throws UserDeadException if the user has disconnected
	 */
	public void sendResponse (final JSONObject result,
			final Integer room, final AbstractUser u)
	throws UserDeadException {
		sendResponse (result, room, null != myUser && null != u
				&& myUser.getUserID () != u.getUserID ());
	}

	/**
	 * Send a response as a future datagram, presumably to a remote
	 * user's thread
	 * 
	 * @param result the JSON object to send to the client
	 * @param room the room number in which the event occurred
	 * @param u ignored…
	 * @throws UserDeadException if the user has already disconnected
	 * 
	 * @deprecated use {@link #sendResponse(JSONObject, Integer)}
	 */
	@Deprecated
	public void sendResponseRemote (final JSONObject result,
			final Integer room, final AbstractUser u)
	throws UserDeadException {
		sendResponse (result, room, true);
	}

	/**
	 * Send notification that an user has joined a room
	 * 
	 * @param room the room that has been joined by an user
	 * @param user the user joining the room
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomEnteredByUser (final AbstractRoom room,
			final AbstractUser user) throws UserDeadException {
		if (room.isLimbo ()) return;
		tattle ("roomJoin\t" + room.getMoniker ());
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject result = new JSONObject ();
			try {
				result.put ("from", "join");
				result.put ("r", room);
				result.put ("user", user.getPublicInfo ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendRawMessage (result.toString (), true);
			return;
		}

		sendRawMessage ("<msg t='sys'><body action='uER' r='"
				+ room.getID () + "'>" + user.toSFSXML ()
				+ "</body></msg>", true);
	}

	/**
	 * Send the user a room list for their current zone
	 * 
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomList () throws UserDeadException {
		sendRoomList (zone, true);
	}

	/**
	 * Send the user a room list for an arbitrary zone
	 * 
	 * @param forZone The zone for which the user will receive a room
	 *            list
	 * @param remote If true, writing to a remote user
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomList (final AbstractZone forZone,
			final boolean remote) throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject result = new JSONObject ();
			final JSONObject rooms = new JSONObject ();
			try {
				result.put ("from", "roomList");
				for (final AbstractRoom room : forZone.getRoomList ()) {
					rooms.put (String.valueOf (room.getID ()), room
							.toJSON ());
				}
				result.put ("rooms", rooms);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendRawMessage (result.toString (), remote);
			return;
		}

		final String zoneXML = forZone.getRoomListSFSXML ();
		sendRawMessage (zoneXML, remote);
	}

	/**
	 * Send a notification that an user has departed from a room
	 * 
	 * @param room The room from which someone has departed
	 * @param user The user who has departed from the room
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomPartedBy (final AbstractRoom room,
			final AbstractUser user) throws UserDeadException {
		if (room.isLimbo ()) return;
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject response = new JSONObject ();
			try {
				response.put ("from", "part");
				response.put ("u", user.getUserID ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendResponse (response, room.getID ());
			return;
		}
		final StringBuilder partMessage = new StringBuilder ();
		partMessage.append ("<msg t='sys'><body action='userGone' r='");
		partMessage.append (room.getID ());
		partMessage.append ("'><user id='");
		partMessage.append (user.getUserID ());
		partMessage.append ("' /></body></msg>");
		sendRawMessage (partMessage.toString (), true);
	}

	/**
	 * Send the user count for the given room
	 * 
	 * @param room The room whose user count is being updated
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomUserCount (final AbstractRoom room)
	throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject response = new JSONObject ();
			try {
				response.put ("from", "userCount");
				response.put ("count", room.getUserCount ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendResponse (response, room.getID ());
			return;
		}
		sendRawMessage ("<msg t='sys'><body action='uCount' r='"
				+ room.getID () + "' u='" + room.getUserCount ()
				+ "'></body></msg>", true);
	}

	/**
	 * @param roomNum The room number for which the variable is being
	 *            set
	 * @param varName The name of the room variable
	 * @param varValue The new value of the variable
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendRoomVar (final int roomNum, final String varName,
			final String varValue) throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject response = new JSONObject ();
			try {
				response.put ("from", "rv");
				response.put ("v", varName);
				response.put ("e", varValue);
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendResponse (response, roomNum);
			return;
		}
		sendRawMessage ("<msg t='sys'><body action='rVarsUpdate' r='"
				+ roomNum + "'><vars><var n='" + varName
				+ "' t='s'><![CDATA[" + varValue
				+ "]]></var></vars></body></msg>", true);
	}

	/**
	 * Send a JSON success packet back to the client
	 * 
	 * @param source the method returning success
	 * @param resultIn additional information to be returned to the
	 *            client
	 * @param u the user responsible for the successful reply (ignored)
	 * @param room the room in which the user is standing (ignored)
	 * @throws JSONException if the success reply can't be encoded in
	 *             JSON form
	 */
	public void sendSuccessReply (final String source,
			final JSONObject resultIn, final AbstractUser u,
			final int room) throws JSONException {
		JSONObject result = resultIn;
		if (null == result) {
			result = new JSONObject ();
		}
		result.put ("status", true);
		result.put ("from", source);

		try {
			sendResponse (result);
		} catch (final UserDeadException e) {
			// Don't ask, don't care
		}
	}

	/**
	 * Send notification that a user has departed from the room
	 * 
	 * @param user The user departing from the room
	 * @param room The room from which the user has departed
	 * @throws UserDeadException if the user has been disconnected
	 * @deprecated use {@link #sendRoomPartedBy(Room, AbstractUser)}
	 */
	@Deprecated
	public void sendUserPart (final AbstractUser user,
			final AbstractRoom room) throws UserDeadException {
		sendRoomPartedBy (room, user);
	}

	/**
	 * Send an update to an user variable
	 * 
	 * @param user The user whose variable has been updated
	 * @param varName The name of the user variable
	 * @param varValue The new value of the user variable
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void sendUserVariable (final User user,
			final String varName, final String varValue)
	throws UserDeadException {
		if (Double.POSITIVE_INFINITY == clientProtocolLanguage) {
			final JSONObject result = new JSONObject ();
			try {
				result.put ("from", "uv");
				result.put ("v", varName);
				result.put ("e", varValue);
				result.put ("u", user.getUserID ());
			} catch (final JSONException e) {
				AppiusClaudiusCaecus.fatalBug (e);
			}
			sendRawMessage (result.toString (), true);
			return;
		}

		sendRawMessage ("<msg t='sys'><body action='uVarsUpdate' r='"
				+ user.getRoomNumber () + "'><user id='"
				+ user.getUserID () + "' /><vars><var n='" + varName
				+ "' t='s'><![CDATA[" + varValue
				+ "]]></var></vars></body></msg>", true);
	}

	/**
	 * @param b true, if the thread is in a busy state and should not be
	 *            interrupted for idle timeout
	 */
	public void setBusyState (final boolean b) {
		busyState = b;
	}

	/**
	 * Set the server's debugging mode on (true) or off (false)
	 * 
	 * @param newDebug True, if the server should be in debug mode;
	 *            else, false
	 */
	public void setDebug (final boolean newDebug) {
		debug = newDebug;
		AppiusConfig.setConfig ("org.starhope.appius.debug",
				debug ? "true" : "false");
	}

	/**
	 * @param thatTime the time of last input from the client
	 */
	public void setLastInputTime (final long thatTime) {
		lastInputTime = thatTime;
	}

	/**
	 * @param amILoggedInNow True, if the thread represents a logged-in
	 *            user
	 */
	public void setLoggedIn (final boolean amILoggedInNow) {
		loggedIn = amILoggedInNow;
	}

	/**
	 * @param smartFoxServerCommProtocolVersion The protocol version to
	 *            be used. This version of the server supports Smart Fox
	 *            Server Pro version 1.58, or Cubist JSON form using the
	 *            value Double.POSITIVE_INFINITY
	 */
	public void setSFSVersion (
			final float smartFoxServerCommProtocolVersion) {
		clientProtocolLanguage = smartFoxServerCommProtocolVersion;
	}

	/**
	 * Set up this thread to execute
	 * 
	 * @throws IOException if the I/O streams can't be initialized
	 * @throws UserDeadException if the user disconnects before setup is
	 *             complete
	 */
	private synchronized void setup ()
	throws IOException, UserDeadException {
		final Socket sock = getSocket ();
		if (null == sock) throw new UserDeadException ();
		out = new PrintWriter (sock.getOutputStream (), true);
		in = new BufferedReader (new InputStreamReader (sock
				.getInputStream ()));
		if (null == out || null == in) throw new UserDeadException ();

		String outputLine;
		try {
			tattle ("//helo//Priming with null input");
			outputLine = processInput ("");
		} catch (final Exception e) {
			AppiusClaudiusCaecus.reportBug (e);
			outputLine = "?ERR\tE\t" + e.toString () + "\n"
			+ AppiusClaudiusCaecus.stringify (e) + "\07\n\0";
		} catch (final Error e) {
			AppiusClaudiusCaecus.reportBug (e.toString () + "\n"
					+ AppiusClaudiusCaecus.stringify (e));
			outputLine = "?ERR\tERR\t" + e.toString () + "\n"
			+ AppiusClaudiusCaecus.stringify (e) + "\07\n\0";
		}
		if (null == outputLine) {
			dropSocketConnection ();
			return;
		}
		if (outputLine.length () > 0) {
			if (isDebug ()) {
				tattle ("//send//" + outputLine);
			}
			sendRawMessage (outputLine, false);
		} else if (isDebug ()) {
			tattle ("// nothing to send //");
		}
	}

	/**
	 * tattle a non-urgent message
	 * 
	 * @param message message
	 */
	public void tattle (final String message) {
		tattle (message, false);
	}

	/**
	 * Print a log entry to STDERR with a great deal of identifiable
	 * detail
	 * 
	 * @param tattle the log entry to be printed
	 * @param urgent Display this message even in non-debug mode (in
	 *            logs)
	 */
	public void tattle (final String tattle, final boolean urgent) {
		if (false == urgent
				&& false == AppiusConfig
				.getConfigBoolOrFalse ("org.starhope.appius.debug"))
			return;
		final StringBuilder tattler = new StringBuilder ();
		tattler.append (new java.util.Date ().toString ());
		tattler.append ('\t');
		if (null == myUser) {
			tattler.append ("prelogin " + preloginCountdown);
			tattler.append ('\t');
		} else {
			tattler.append ("user #" + myUser.getUserID () + " “"
					+ myUser.getUserName () + "”");
			tattler.append (" S#" + myUser.getMySerial ());
			tattler.append ('\t');
			if (null != myUser.getRoom ()) {
				tattler.append ("room “"
						+ myUser.getRoom ().getMoniker () + "” in ");
			}
		}
		if (null != zone) {
			tattler.append ("zone “" + zone.getName () + "”");
		}
		tattler.append ('\t');
		if (null != socket) {
			tattler.append (socket.getRemoteSocketAddress ()
					.toString ());
		}
		tattler.append ('\t');
		tattler.append (tattle);
		tattler.append ('\n');
		System.err.print (tattler);
	}

	/**
	 * Propagate a metronome tick
	 * 
	 * @param t The value of System.currentTimeMillis at the start of
	 *            this tick
	 * @param dT The delta-T since the prior tick
	 * @throws UserDeadException if the user has been disconnected
	 */
	public void tick (final long t, final long dT)
	throws UserDeadException {
		final long tIdle = t - lastInputTime;
		if ( !busyState) {
			if (tick_checkIdleKick (tIdle)) return;
			if (tick_checkIdleWarnTime (tIdle)) return;
		}
		if (t - tLastNudge > AppiusConfig.getNudgeTime ()) {
			if (null == myUser) {
				/* Prelogin thread */
				if (null == socket) throw new UserDeadException ();
				if ( !socket.isConnected ()) {
					dropSocketConnection ();
					throw new UserDeadException ();
				}
			}

			for (final Zone z : AppiusClaudiusCaecus.zones.values ()) {
				z.checkZonesForSpawn ();
			}
		}
	}

	/**
	 * Check whether the user has been idle for too long, and kick them
	 * offline if so
	 * 
	 * @param tIdle Time that the user has been idle (milliseconds)
	 * @return true, if the user was kicked off
	 * @throws UserDeadException if the user is disconnected
	 */
	private boolean tick_checkIdleKick (final long tIdle)
	throws UserDeadException {
		if (tIdle > AppiusConfig.getIdleKickTime ()) {
			sendAdminDisconnect (LibMisc.getText ("idleKick"), "Idle",
					"System", "idleKick");
			return true;
		}
		return false;
	}

	/**
	 * Check how long the user has been idle, and send a warning if the
	 * time idle has exceeded a limit
	 * 
	 * @param tIdle The time that this connection or user has been idle
	 * @return true, if the user was warned; false, if not
	 * @throws UserDeadException if the user went away
	 */
	private boolean tick_checkIdleWarnTime (final long tIdle)
	throws UserDeadException {
		if (tIdle > AppiusConfig.getIdleWarnTime ()
				&& idleWarned == lastInputTime) {
			sendAdminMessage (LibMisc.getText ("idleWarn"), false);
			idleWarned = lastInputTime;
			return true;
		}
		return false;
	}

	/**
	 * This returns a plethora of debugging-useful information about
	 * this particular server thread.
	 * 
	 * @see java.lang.Thread#toString()
	 */
	@Override
	public String toString () {
		final StringBuilder res = new StringBuilder (super.toString ());
		res.append ("\nAppius Claudius Caecus:");
		if (null != myUser) {
			res.append ("\n\tUser:\t");
			res.append (myUser.getUserName ());
			res.append (" #");
			res.append (myUser.getUserID ());
		}
		if (loggedIn) {
			res.append ("\n\t\tLogged in");
		} else {
			res.append ("\n\tPrelogin countdown:\t");
			res.append (preloginCountdown);
		}
		if (null != zone) {
			res.append ("\n\tZone:\t");
			res.append (zone.getName ());
		}
		res.append ("\n\tProtocol language:\t");
		res.append (clientProtocolLanguage);
		res.append ("\n\tDebug mode:\t");
		res.append (debug);
		res.append ("\n\tFuture datagrams enqueued:\t");
		res.append (futureDatagrams.size ());
		res.append ("\n\tLast input:\t");
		res.append (lastInputTime);
		res.append ("\n\tLast nudge:\t");
		res.append (tLastNudge);
		res.append ("\n\tState code:\t");
		res.append (state);
		res.append ('\n');
		return res.toString ();
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread,
	 *      java.lang.Throwable)
	 */
	public void uncaughtException (final Thread t, final Throwable e) {
		AppiusClaudiusCaecus.reportBug ("Uncaught Exception in "
				+ t.getName (), e);
	}

	/**
	 * Create a message string informing the user that an error has
	 * occurred, and instructing them to contact Customer Service.
	 * 
	 * @param string The debugging code to append to the message
	 * @return A string to return to the user
	 */
	private String userDebug (final String string) {
		// TODO: getText
		final StringBuilder debugString = new StringBuilder (
		"\nWe're sorry that you have encountered an unexpected error. So that we can help you better, please contact Customer Service and give them the following code:\n");
		debugString.append (string);
		debugString.append (' ');
		debugString.append (AppiusClaudiusCaecus.getRev ());
		debugString.append ('\n');
		return debugString.toString ();
	}

}
