com.springrts.springls.Client.java Source code

Java tutorial

Introduction

Here is the source code for com.springrts.springls.Client.java

Source

/*
   Copyright (c) 2005 Robin Vobruba <hoijui.quaero@gmail.com>
    
   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 2 of the License, or
   (at your option) any later version.
    
   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.
    
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.springrts.springls;

import com.springrts.springls.ip2country.IP2CountryService;
import com.springrts.springls.util.Misc;
import com.springrts.springls.util.ProtocolUtil;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import org.apache.commons.configuration.Configuration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Client instance can be seen as a session of a human user, which has logged
 * into the Lobby using his <code>Account</code> info.
 *
 * @see Account
 * @author Betalord
 * @author hoijui
 */
public class Client extends TeamController implements ContextReceiver {

    private static final Logger LOG = LoggerFactory.getLogger(Client.class);

    /**
     * Indicates a message is not using an ID
     * (see protocol description on message/command IDs)
     */
    public static final int NO_MSG_ID = -1;

    /**
     * Indicates that no script-password is set.
     */
    public static final String NO_SCRIPT_PASSWORD = "";

    /**
     * If false, then this client is not "valid" anymore.
     * We already killed him and closed his socket.
     */
    private boolean alive = false;
    /**
     * When we schedule client for kill (via Clients.killClientDelayed(),
     * for example) this flag is set to true.
     * When true, we do not read or send any data to this client.
     */
    private boolean halfDead = false;

    private Account account;
    /**
     * External IP
     */
    private InetAddress ip;
    /**
     * Local IP, which has to be sent with LOGIN command
     * The server can not figure out the clients local IP by himself of course.
     */
    private InetAddress localIp;
    /**
     * Public UDP source port used with some NAT traversal techniques,
     * e.g. "hole punching".
     */
    private int udpSourcePort;
    private boolean inGame;
    private boolean away;
    /**
     * ID of the battle in which this client is participating.
     * Has to be -1 if not participating in any battle.
     */
    private int battleID;
    /**
     * ID of the battle which this client is requesting to join.
     * Must be -1 if not requesting to join any battle.
     */
    private int requestedBattleID;
    /**
     * List of channels user is participating in.
     */
    private List<Channel> channels = new ArrayList<Channel>();

    private SocketChannel sockChan;
    private SelectionKey selKey;
    private StringBuilder recvBuf;
    /**
     * This is the message/command ID used when sending command
     * as described in the "lobby protocol description" document.
     * Use setSendMsgId() and resetSendMsgId() methods to manipulate it.
     * NO_MSG_ID means no ID is used.
     */
    private int myMsgId = NO_MSG_ID;
    /**
     * Queue of "delayed data".
     * We failed sending this the first time, so we will have to try sending it
     * again some time later.
     */
    private Queue<ByteBuffer> sendQueue = new LinkedList<ByteBuffer>();
    /**
     * Temporary StringBuilder used by some internal methods.
     * @see beginFastWrite()
     * @see endFastWrite()
     */
    private StringBuilder fastWrite;

    /**
     * In milliseconds.
     * Used internally to remember time when the user entered the game.
     * @see java.lang.System#currentTimeMillis()
     */
    private long inGameTime;
    /**
     * Specifying the origin of the user. So far, only the country part is used.
     */
    private Locale locale;
    /**
     * In MHz if possible, or in MHz*1.4 if AMD.
     * 0 means the client can not figure out its CPU speed.
     */
    private int cpu;
    /**
     * e.g. "TASClient 1.0" (gets updated when server receives LOGIN command)
     */
    private String lobbyVersion;
    /**
     * How many bytes did this client send to us since he logged in.
     */
    private long receivedSinceLogin = 0;
    /**
     * Time (in milli-seconds) when we last heard from client
     * (last data received).
     * @see java.lang.System#currentTimeMillis()
     */
    private long timeOfLastReceive;
    /**
     * Does the client accept accountIDs in ADDUSER command?
     */
    private boolean acceptAccountIDs;
    /**
     * Does the client accept JOINBATTLEREQUEST command?
     */
    private boolean handleBattleJoinAuthorization;

    private Context context = null;

    /**
     * Script password for the battle this client currently is in.
     * If it is not currently in a battle, this is irrelevant.
     * The script password is used for spoof-protection, which means
     * someone illegally joining the battle under wrong user-name.
     */
    private String scriptPassword;

    /**
     * Does the client accept the scriptPassord argument to the
     * JOINEDBATTLE command?
     */
    private boolean scriptPasswordSupported;

    /**
     * A list of compatibility-flags, each representing a certain minor change
     * in protocol since the last protocol version number release.
     * These features all have to be implemented in a backwards compatible way,
     * so lobby clients not supporting them are still able to function normally.
     * This is comparable to IRC user/channel flags.
     * By default, all the optional functionalities are considered
     * as not supported by the client.
     *
     * See the "Recent Changes" section or the LOGIN command
     * in the lobby protocol documentation for a list of the current flags.
     */
    private List<String> compatFlags;

    public Client(SocketChannel sockChan) {

        this.alive = true;

        // no info on user/pass, zero access
        this.account = new Account();
        this.sockChan = sockChan;
        this.ip = sockChan.socket().getInetAddress();
        // this fixes the issue with local user connecting to the server at
        // "127.0.0.1", as he can not host battles with that ip
        if (ip.isLoopbackAddress()) {
            InetAddress newIP = Misc.getLocalIpAddress();
            if (newIP != null) {
                ip = newIP;
            } else {
                LOG.warn("Could not resolve local IP address."
                        + " The user may have problems with hosting battles.");
            }
        }
        localIp = ip; // will be changed later once the client logs in
        udpSourcePort = 0; // yet unknown
        selKey = null;
        recvBuf = new StringBuilder();
        inGame = false;
        away = false;
        locale = ProtocolUtil.countryToLocale(ProtocolUtil.COUNTRY_UNKNOWN);
        inGameTime = 0;
        battleID = Battle.NO_BATTLE_ID;
        requestedBattleID = Battle.NO_BATTLE_ID;
        cpu = 0;
        scriptPassword = NO_SCRIPT_PASSWORD;
        scriptPasswordSupported = false;
        compatFlags = new ArrayList<String>(0);

        timeOfLastReceive = System.currentTimeMillis();
    }

    @Override
    public void receiveContext(Context context) {

        this.context = context;

        // TODO when bundle-context is available in ctor, move this code there
        IP2CountryService ip2CountryService = context.getService(IP2CountryService.class);
        if (ip2CountryService != null) {
            setLocale(ip2CountryService.getLocale(ip));
        }

        Set<String> supportedCompFlags = context.getServer().getSupportedCompFlags();
        supportedCompFlags.add("a");
        supportedCompFlags.add("b");
        supportedCompFlags.add("sp");
    }

    @Override
    public int hashCode() {

        int hash = 5;
        hash = 67 * hash + (this.sockChan != null ? this.sockChan.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {

        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Client other = (Client) obj;
        if (this.sockChan != other.getSockChan()
                && (this.sockChan == null || !this.sockChan.equals(other.getSockChan()))) {
            return false;
        }
        return true;
    }

    /**
     * Any messages sent via sendLine() method will contain this ID.
     * See "lobby protocol description" document for more info
     * on message/command IDs.
     */
    public void setSendMsgId(int msgId) {
        this.myMsgId = msgId;
    }

    public void resetSendMsgId(int msgId) {
        this.myMsgId = msgId;
    }

    /**
     * Will prefix the message with a msgId value, if it was previously
     * set via setSendMsgId() method.
     */
    public boolean sendLine(String text) {
        return sendLine(text, myMsgId);
    }

    /**
     * @param msgId overrides any previously set message ID,
     *   use NO_MSG_ID for none.
     * @see #setSendMsgId(int msgId)
     */
    private boolean sendLine(String text, int msgId) {

        if (!alive || halfDead) {
            return false;
        }

        StringBuilder data = new StringBuilder();

        // prefix message with a message ID:
        if (msgId != NO_MSG_ID) {
            data.append("#").append(msgId).append(" ");
        }
        data.append(text);

        if (fastWrite != null) {
            if (fastWrite.length() != 0) {
                fastWrite.append(Misc.EOL);
            }
            fastWrite.append(data);
            return true;
        }

        if (LOG.isTraceEnabled()) {
            String nameOrIp = (account.getAccess() != Account.Access.NONE) ? account.getName()
                    : ip.getHostAddress();
            LOG.trace("[->{}] \"{}\"", nameOrIp, data);
        }

        try {
            // prepare data and add it to the send queue

            data.append(Misc.EOL);

            if ((sockChan == null) || (!sockChan.isConnected())) {
                LOG.warn("SocketChannel is not ready to be written to." + " Killing the client next loop ...");
                context.getClients().killClientDelayed(this, "Quit: undefined connection error");
                return false;
            }

            ByteBuffer buf;
            try {
                buf = context.getServer().getAsciiEncoder().encode(CharBuffer.wrap(data));
            } catch (CharacterCodingException ex) {
                LOG.warn("Unable to encode message. Killing the client next" + " loop ...", ex);
                context.getClients().killClientDelayed(this, "Quit: undefined encoder error");
                return false;
            }

            if (!sendQueue.isEmpty()) {
                sendQueue.add(buf);
            } else {
                sendQueue.add(buf);
                boolean empty = tryToFlushData();
                if (!empty) {
                    context.getClients().enqueueDelayedData(this);
                }
            }
        } catch (Exception ex) {
            LOG.error("Failed sending data (undefined). Killing the client next" + " loop ...", ex);
            context.getClients().killClientDelayed(this, "Quit: undefined connection error");
            return false;
        }
        return true;
    }

    public void sendWelcomeMessage() {

        Configuration conf = context.getService(Configuration.class);

        // the welcome messages command-name is hardcoded to TASSERVER
        // XXX maybe change TASSERVER to WELCOME or the like -> protocol change
        sendLine(String.format("TASSERVER %s %s %d %d", conf.getString(ServerConfiguration.LOBBY_PROTOCOL_VERSION),
                conf.getString(ServerConfiguration.ENGINE_VERSION), conf.getInt(ServerConfiguration.NAT_PORT),
                conf.getBoolean(ServerConfiguration.LAN_MODE) ? 1 : 0));
    }

    /** Should only be called by Clients.killClient() method! */
    public void disconnect() {

        if (!alive) {
            LOG.error("PROBLEM DETECTED: disconnecting dead client." + " Skipping ...");
            return;
        }

        try {
            sockChan.close();
        } catch (Exception ex) {
            LOG.error("Failed disconnecting socket!", ex);
        }

        sockChan = null;
        selKey = null;
    }

    /**
     * joins client to <chanName> channel. If channel with that name
     * does not exist, it is created. Method returns channel object as a result.
     * If client is already in the channel, it returns null as a result.
     * This method does not check for a correct key in case the channel is
     * locked, caller of this method should do that before calling it.
     * This method also does not do any notifying of other clients in the
     * channel, the caller must do all that.
     */
    public Channel joinChannel(String chanName) {

        Channel chan = context.getChannels().getChannel(chanName);
        if (chan == null) {
            chan = new Channel(chanName);
            context.getChannels().addChannel(chan);
        } else if (this.channels.indexOf(chan) != -1) {
            // already in the channel
            return null;
        }

        chan.addClient(this);
        this.channels.add(chan);
        return chan;
    }

    /**
     * Removes this client from a specific channel and notifies all other
     * clients in that channel about it.
     * If this was the last client in the channel, then the channel is removed
     * from channels list.
     * @param reason may be left blank ("") if no reason is to be given.
     */
    public boolean leaveChannel(Channel chan, String reason) {

        boolean left = chan.removeClient(this);

        if (left) {
            if (chan.getClientsSize() == 0) {
                // since channel is empty, there is no point in keeping it
                // in a channels list. If we would keep it, the channels list
                // would grow larger and larger in time.
                // We don't want that!
                context.getChannels().removeChannel(chan);
            } else {
                StringBuilder message = new StringBuilder("LEFT ");
                message.append(chan.getName()).append(" ");
                message.append(this.account.getName());
                if (!reason.isEmpty()) {
                    message.append(" ").append(reason);
                }

                String messageStr = message.toString();
                for (int i = 0; i < chan.getClientsSize(); i++) {
                    chan.getClient(i).sendLine(messageStr);
                }
            }
            this.channels.remove(chan);
        }

        return left;
    }

    /**
     * Calls leaveChannel() for every channel client is participating in.
     * Also notifies all clients of his departure.
     * Also see comments for leaveChannel() method.
     */
    public void leaveAllChannels(String reason) {

        while (!channels.isEmpty()) {
            leaveChannel(channels.get(0), reason);
        }
        this.channels.clear();
    }

    /**
     * Will search the list of channels this user is participating in
     * and return the specified channel or 'null' if client is not participating
     * in this channel.
     */
    public Channel getChannel(String chanName) {

        for (int i = 0; i < channels.size(); i++) {
            if (channels.get(i).getName().equals(chanName)) {
                return channels.get(i);
            }
        }
        return null;
    }

    /**
     * Tries to send the data from the sendQueue.
     * @return true if all data has been flushed; false otherwise.
     */
    public boolean tryToFlushData() {

        if (!alive || halfDead) {
            // disregard any other scheduled writes:
            while (sendQueue.size() != 0) {
                sendQueue.remove();
            }
            return true; // no more data left to be flushed, so return true
        }

        ByteBuffer buf;
        while ((buf = sendQueue.peek()) != null) {
            try {
                sockChan.write(buf);

                if (buf.hasRemaining()) {
                    // This happens when send buffer is full and no more data
                    // can be written to it.
                    // Lets just skip it without removing the packet
                    // from the send queue (we will retry sending it later).
                    break;
                }
                // remove element from queue (it was sent entirely)
                sendQueue.remove();
            } catch (ClosedChannelException ccex) {
                // no point sending the rest to the closed channel
                if (alive) {
                    context.getClients().killClientDelayed(this, "Quit: socket channel closed exception");
                }
                break;
            } catch (IOException ioex) {
                if (alive) {
                    context.getClients().killClientDelayed(this, "Quit: socket channel closed exception");
                }
                break;
            }
        }

        return sendQueue.size() == 0;
    }

    public void beginFastWrite() {

        if (fastWrite != null) {
            LOG.error("Invalid use of beginFastWrite()." + " Check your code! Shutting down the server ...");
            context.getServerThread().closeServerAndExit();
        }

        fastWrite = new StringBuilder();
    }

    public void endFastWrite() {

        if (fastWrite == null) {
            LOG.error("Invalid use of endFastWrite()." + " Check your code! Shutting down the server ...");
            context.getServerThread().closeServerAndExit();
        }

        String data = fastWrite.toString();
        fastWrite = null;
        if (data.isEmpty()) {
            return;
        }
        sendLine(data);
    }

    // various methods dealing with client status

    public boolean isInGame() {
        return inGame;
    }

    public void setInGame(boolean inGame) {
        this.inGame = inGame;
    }

    public boolean isAway() {
        return away;
    }

    public void setAway(boolean away) {
        this.away = away;
    }

    /**
     * The access status tells us whether this client is a server moderator or
     * not.
     * This is only used for generating the status bits for the CLIENTSTATUS
     * command.
     */
    private boolean isAccess() {
        return getAccount().getAccess().isAtLeast(Account.Access.PRIVILEGED);
    }

    /**
     * If false, then this client is not "valid" anymore.
     * We already killed him and closed his socket.
     * @return the alive
     */
    public boolean isAlive() {
        return alive;
    }

    /**
     * If false, then this client is not "valid" anymore.
     * We already killed him and closed his socket.
     * @param alive the alive to set
     */
    public void setAlive(boolean alive) {
        this.alive = alive;
    }

    /**
     * When we schedule client for kill (via Clients.killClientDelayed(),
     * for example) this flag is set to true.
     * When true, we do not read or send any data to this client.
     * @return the halfDead
     */
    public boolean isHalfDead() {
        return halfDead;
    }

    /**
     * When we schedule client for kill (via Clients.killClientDelayed(),
     * for example) this flag is set to true.
     * When true, we do not read or send any data to this client.
     * @param halfDead the halfDead to set
     */
    public void setHalfDead(boolean halfDead) {
        this.halfDead = halfDead;
    }

    /**
     * @return the account
     */
    public Account getAccount() {
        return account;
    }

    /**
     * @param account the account to set
     */
    public void setAccount(Account account) {
        this.account = account;
    }

    /**
     * External IP
     * @return the IP
     */
    public InetAddress getIp() {
        return ip;
    }

    /**
     * External IP
     * @param ip the IP to set
     */
    public void setIp(InetAddress ip) {
        this.ip = ip;
    }

    /**
     * Local IP, which has to be sent with LOGIN command
     * The server can not figure out the clients local IP by himself of course.
     * @return the localIP
     */
    public InetAddress getLocalIp() {
        return localIp;
    }

    /**
     * Local IP, which has to be sent with LOGIN command
     * The server can not figure out the clients local IP by himself of course.
     * @param localIp the local IP to set
     */
    public void setLocalIP(InetAddress localIp) {
        this.localIp = localIp;
    }

    /**
     * Public UDP source port used with some NAT traversal techniques,
     * e.g. "hole punching".
     * @return the udpSourcePort
     */
    public int getUdpSourcePort() {
        return udpSourcePort;
    }

    /**
     * Public UDP source port used with some NAT traversal techniques,
     * e.g. "hole punching".
     * @param udpSourcePort the udpSourcePort to set
     */
    public void setUdpSourcePort(int udpSourcePort) {
        this.udpSourcePort = udpSourcePort;
    }

    /**
     * See the 'MYSTATUS' command for valid values
     * @return the status
     */
    public int getStatus() {

        int status = 0;

        status += isInGame() ? 1 : 0;
        status += (isAway() ? 1 : 0) << 1;
        status += getAccount().getRank().ordinal() << 2;
        status += (isAccess() ? 1 : 0) << 5;
        status += (getAccount().isBot() ? 1 : 0) << 6;

        return status;
    }

    /**
     * See the 'MYSTATUS' command for valid values
     * @param status the status to set
     * @param priviledged rank, access and bot are only changed if this is true
     */
    public void setStatus(int status, boolean priviledged) {

        setInGame((status & 0x1) == 1);
        setAway(((status & 0x2) >> 1) == 1);

        // This method is only used in MYSTATUS, which priviledged == false.
        // Therefore, the following is never used, and only stays here for
        // historical reasons.
        if (priviledged) {
            // use the highest rank, if a too high value was specified
            //         int rankIndex = (status & 0x1C) >> 2;
            //         Account.Rank newRank = (rankIndex < Account.Rank.values().length)
            //               ? Account.Rank.values()[rankIndex]
            //               : Account.Rank.values()[Account.Rank.values().length - 1];
            //         getAccount().setRank(newRank);
            //         setAccess(((status & 0x20) >> 5) == 1);
            getAccount().setBot(((status & 0x40) >> 6) == 1);
        }
    }

    /**
     * ID of the battle in which this client is participating.
     * Has to be -1 if not participating in any battle.
     * @return the battleID
     */
    public int getBattleID() {
        return battleID;
    }

    /**
     * ID of the battle in which this client is participating.
     * Has to be -1 if not participating in any battle.
     * @param battleID the battleID to set
     */
    public void setBattleID(int battleID) {

        this.battleID = battleID;
        if (battleID == Battle.NO_BATTLE_ID) {
            setScriptPassword(NO_SCRIPT_PASSWORD);
        }
    }

    /**
     * ID of the battle which this client is requesting to join.
     * Must be -1 if not requesting to join any battle.
     * @return the requestedBattleID
     */
    public int getRequestedBattleID() {
        return requestedBattleID;
    }

    /**
     * ID of the battle which this client is requesting to join.
     * Must be -1 if not requesting to join any battle.
     * @param requestedBattleID the requestedBattleID to set
     */
    public void setRequestedBattleID(int requestedBattleID) {
        this.requestedBattleID = requestedBattleID;
    }

    /**
     * @return the sockChan
     */
    public SocketChannel getSockChan() {
        return sockChan;
    }

    /**
     * @param selKey the selKey to set
     */
    public void setSelKey(SelectionKey selKey) {
        this.selKey = selKey;
    }

    public void appendToRecvBuf(String received) {
        recvBuf.append(received);
    }

    private static boolean isWhiteSpace(char c) {
        return (" \n\r\t\f".indexOf(c) != -1);
    }

    private static void deleteLeadingWhiteSpace(StringBuilder str) {

        int wsPos = 0;
        while ((wsPos < str.length()) && isWhiteSpace(str.charAt(wsPos))) {
            wsPos++;
        }
        str.delete(0, wsPos);
    }

    /**
     * Removes carriage-return chars (first part of windows EOL "\r\n").
     * @param str where to search in
     * @param posUntil only chars from 0 until this position are searched
     * @return number of deleted chars
     */
    private static int deleteCarriageReturnChars(StringBuilder str, int posUntil) {
        int deleted = 0;

        int rPos = str.lastIndexOf("\r", posUntil);
        while (rPos != -1) {
            str.deleteCharAt(rPos);
            deleted++;
            rPos = str.lastIndexOf("\r", rPos);
        }

        return deleted;
    }

    /**
     * Tries to read a line from the clients input buffer.
     * If the this returns non-<code>null</code>, then the line returned is
     * already removed from the buffer.
     * @return the older line from the clients input buffer or
     *   <code>null</code>, if there is no full line available.
     */
    public String readLine() {

        String line = null;

        deleteLeadingWhiteSpace(recvBuf);
        if (recvBuf.length() > 0) {
            int nPos = recvBuf.indexOf("\n");
            if (nPos != -1) {
                int deleted = deleteCarriageReturnChars(recvBuf, nPos);

                line = recvBuf.substring(0, nPos - deleted);
                recvBuf.delete(0, nPos - deleted);
            }
        }

        return line;
    }

    /**
     * In milliseconds.
     * Used internally to remember time when the user entered the game.
     * @see java.lang.System#currentTimeMillis()
     * @return the inGameTime
     */
    public long getInGameTime() {
        return inGameTime;
    }

    /**
     * In milliseconds.
     * Used internally to remember time when the user entered the game.
     * @see java.lang.System#currentTimeMillis()
     * @param inGameTime the inGameTime to set
     */
    public void setInGameTime(long inGameTime) {
        this.inGameTime = inGameTime;
    }

    /**
     * Specifying the origin of the user. So far, only the country part is used.
     * @return the locale specifying the country
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Specifying the origin of the user. So far, only the country part is used.
     * @param locale the locale specifying the country
     */
    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    /**
     * Two letter country code, as defined in ISO 3166-1 alpha-2.
     * "XX" is used for unknown country.
     * @return the country
     */
    public String getCountry() {

        if ((locale == null) || locale.getCountry().isEmpty()) {
            return ProtocolUtil.COUNTRY_UNKNOWN;
        } else {
            return locale.getCountry();
        }
    }

    /**
     * Two letter country code, as defined in ISO 3166-1 alpha-2.
     * "XX" is used for unknown country.
     * @param country the country to set
     */
    public void setCountry(String country) {
        ProtocolUtil.countryToLocale(country);
    }

    /**
     * In MHz if possible, or in MHz*1.4 if AMD.
     * 0 means the client can not figure out its CPU speed.
     * @return the CPU
     */
    public int getCpu() {
        return cpu;
    }

    /**
     * In MHz if possible, or in MHz*1.4 if AMD.
     * 0 means the client can not figure out its CPU speed.
     * @param cpu the CPU to set
     */
    public void setCpu(int cpu) {
        this.cpu = cpu;
    }

    /**
     * e.g. "TASClient 1.0" (gets updated when server receives LOGIN command)
     * @return the lobbyVersion
     */
    public String getLobbyVersion() {
        return lobbyVersion;
    }

    /**
     * e.g. "TASClient 1.0" (gets updated when server receives LOGIN command)
     * @param lobbyVersion the lobbyVersion to set
     */
    public void setLobbyVersion(String lobbyVersion) {
        this.lobbyVersion = lobbyVersion;
    }

    /**
     * Time (in milli-seconds) when we last heard from client
     * (last data received).
     * @see java.lang.System#currentTimeMillis()
     * @return the timeOfLastReceive
     */
    public long getTimeOfLastReceive() {
        return timeOfLastReceive;
    }

    /**
     * Time (in milli-seconds) when we last heard from client
     * (last data received).
     * @see java.lang.System#currentTimeMillis()
     * @param timeOfLastReceive the timeOfLastReceive to set
     */
    public void setTimeOfLastReceive(long timeOfLastReceive) {
        this.timeOfLastReceive = timeOfLastReceive;
    }

    /**
     * Returns the clients script password for the battle it currently is in.
     * If it is not currently in a battle, this is irrelevant.
     * The script password is used for spoof-protection, which means
     * someone illegally joining the battle under wrong user-name.
     */
    public String getScriptPassword() {
        return scriptPassword;
    }

    /**
     * Sets the clients script password for the battle it currently is in.
     * If it is not currently in a battle, this is irrelevant.
     * The script password is used for spoof-protection, which means
     * someone illegally joining the battle under wrong user-name.
     */
    public void setScriptPassword(String scriptPassword) {
        this.scriptPassword = scriptPassword;
    }

    /**
     * Does the client accept accountIDs in ADDUSER command?
     * @return the acceptAccountIDs
     */
    public boolean isAcceptAccountIDs() {
        return acceptAccountIDs;
    }

    /**
     * Does the client accept accountIDs in ADDUSER command?
     * @param acceptAccountIDs the acceptAccountIDs to set
     */
    private void setAcceptAccountIDs(boolean acceptAccountIDs) {
        this.acceptAccountIDs = acceptAccountIDs;
    }

    /**
     * Does the client accept JOINBATTLEREQUEST command?
     * @return the handleBattleJoinAuthorization
     */
    public boolean isHandleBattleJoinAuthorization() {
        return handleBattleJoinAuthorization;
    }

    /**
     * Does the client accept JOINBATTLEREQUEST command?
     */
    private void setHandleBattleJoinAuthorization(boolean handleBattleJoinAuthorization) {
        this.handleBattleJoinAuthorization = handleBattleJoinAuthorization;
    }

    /**
     * Does the client accept the scriptPassord argument to the
     * JOINEDBATTLE command?
     */
    public boolean isScriptPassordSupported() {
        return scriptPasswordSupported;
    }

    /**
     * Does the client accept the scriptPassord argument to the
     * JOINEDBATTLE command?
     */
    private void setScriptPassordSupported(boolean supported) {
        scriptPasswordSupported = supported;
    }

    /**
     * How much data did this client send to us since he logged in.
     * This is used with anti-flood protection.
     * @return the number of bytes received from this client since login.
     */
    public long getReceivedSinceLogin() {
        return receivedSinceLogin;
    }

    /**
     * Adds to the number of bytes received from this client.
     * @param nBytes to add number of bytes
     */
    public void addReceived(long nBytes) {

        receivedSinceLogin += nBytes;
    }

    /**
     * A list of compatibility-flags, each representing a certain minor change
     * in protocol since the last protocol version number release.
     * These features all have to be implemented in a backwards compatible way,
     * so lobby clients not supporting them are still able to function normally.
     * This is comparable to IRC user/channel flags.
     * By default, all the optional functionalities are considered
     * as not supported by the client.
     *
     * See the "Recent Changes" section or the LOGIN command
     * in the lobby protocol documentation for a list of the current flags.
     * @return the compatFlags
     */
    public List<String> getCompatFlags() {
        return compatFlags;
    }

    /**
     * A list of compatibility-flags, each representing a certain minor change
     * in protocol since the last protocol version number release.
     * @see #getCompatFlags
     * @param compatFlags the compatFlags to set
     */
    public void setCompatFlags(List<String> compatFlags) {

        this.compatFlags = Collections.unmodifiableList(compatFlags);

        // protocol version 0.37-SNAPSHOT (after 0.36) compat-flags:
        setAcceptAccountIDs(compatFlags.contains("a"));
        setHandleBattleJoinAuthorization(compatFlags.contains("b"));
        setScriptPassordSupported(compatFlags.contains("sp"));
    }
}