/*
* The Mana World Server
* Copyright 2004 The Mana World Development Team
*
* This file is part of The Mana World.
*
* The Mana World 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
* any later version.
*
* The Mana World 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 The Mana World; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id$
*/
#include "accounthandler.h"
#include "account.h"
#include "chathandler.h"
#include "configuration.h"
#include "connectionhandler.h"
#include "debug.h"
#include "gamehandler.h"
#include "messagein.h"
#include "messageout.h"
#include "netcomputer.h"
#include "storage.h"
#include "utils/logger.h"
#include "utils/stringfilter.h"
using tmwserv::Account;
using tmwserv::AccountPtr;
using tmwserv::BeingPtr;
using tmwserv::Storage;
class AccountClient: public NetComputer
{
public:
/**
* Constructor.
*/
AccountClient(AccountHandler *, ENetPeer *);
/**
* Destructor.
*/
~AccountClient();
/**
* Set the account associated with the connection
*/
void setAccount(AccountPtr acc);
/**
* Unset the account associated with the connection
*/
void unsetAccount();
/**
* Get account associated with the connection.
*/
AccountPtr getAccount() { return mAccountPtr; }
/**
* Set the selected character associated with connection.
*/
void setCharacter(BeingPtr ch);
/**
* Deselect the character associated with connection.
*/
void unsetCharacter();
/**
* Get character associated with the connection
*/
BeingPtr getCharacter() { return mCharacterPtr; }
private:
/** Account associated with connection */
AccountPtr mAccountPtr;
/** Selected character */
BeingPtr mCharacterPtr;
};
AccountClient::AccountClient(AccountHandler *handler, ENetPeer *peer):
NetComputer(handler, peer),
mAccountPtr(NULL),
mCharacterPtr(NULL)
{
}
AccountClient::~AccountClient()
{
unsetAccount();
}
void AccountClient::setAccount(AccountPtr acc)
{
unsetAccount();
mAccountPtr = acc;
}
void AccountClient::setCharacter(BeingPtr ch)
{
unsetCharacter();
mCharacterPtr = ch;
}
void AccountClient::unsetAccount()
{
unsetCharacter();
mAccountPtr = AccountPtr(NULL);
}
void AccountClient::unsetCharacter()
{
if (mCharacterPtr.get() == NULL) return;
mCharacterPtr = BeingPtr(NULL);
}
NetComputer *AccountHandler::computerConnected(ENetPeer *peer)
{
return new AccountClient(this, peer);
}
void AccountHandler::computerDisconnected(NetComputer *comp)
{
delete comp;
}
/**
* Generic interface convention for getting a message and sending it to the
* correct subroutines. Account handler takes care of determining the
* current step in the account process, be it creation, setup, or login.
*/
void AccountHandler::processMessage(NetComputer *comp, MessageIn &message)
{
AccountClient &computer = *static_cast< AccountClient * >(comp);
Storage &store = Storage::instance("tmw");
#if defined (SQLITE_SUPPORT)
// Reopen the db in this thread for sqlite, to avoid
// Library Call out of sequence problem due to thread safe.
store.setUser(config.getValue("dbuser", ""));
store.setPassword(config.getValue("dbpass", ""));
store.close();
store.open();
#endif
MessageOut result;
switch (message.getId())
{
case PAMSG_LOGIN:
{
std::string clientVersion = message.readString();
std::string username = message.readString();
std::string password = message.readString();
LOG_INFO(username << " is trying to login.", 1);
result.writeShort(APMSG_LOGIN_RESPONSE);
if (clientVersion < config.getValue("clientVersion", "0.0.0"))
{
LOG_INFO("Client has an unsufficient version number to login.", 1);
result.writeByte(LOGIN_INVALID_VERSION);
break;
}
if (stringFilter->findDoubleQuotes(username))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(username << ": has got double quotes in it.", 1);
break;
}
if (computer.getAccount().get() != NULL) {
LOG_INFO("Already logged in as " << computer.getAccount()->getName()
<< ".", 1);
LOG_INFO("Please logout first.", 1);
result.writeByte(ERRMSG_FAILURE);
break;
}
if (getClientNumber() >= MAX_CLIENTS )
{
// Too much clients logged in.
LOG_INFO("Client couldn't login. Already has " << MAX_CLIENTS
<< " logged in.", 1);
result.writeByte(LOGIN_SERVER_FULL);
break;
}
// see if the account exists
tmwserv::AccountPtr acc = store.getAccount(username);
if (!acc.get() || acc->getPassword() != password) {
// account doesn't exist -- send error to client
LOG_INFO(username << ": Account does not exist or the password is invalid.", 1);
result.writeByte(ERRMSG_INVALID_ARGUMENT);
break;
}
LOG_INFO("Login OK by " << username, 1);
// Associate account with connection
computer.setAccount(acc);
result.writeByte(ERRMSG_OK);
// Return information about available characters
tmwserv::Beings &chars = computer.getAccount()->getCharacters();
result.writeByte(chars.size());
LOG_INFO(username << "'s account has " << chars.size() << " character(s).", 1);
for (unsigned int i = 0; i < chars.size(); i++)
{
result.writeString(chars[i]->getName());
result.writeByte(unsigned(short(chars[i]->getGender())));
result.writeByte(chars[i]->getHairStyle());
result.writeByte(chars[i]->getHairColor());
result.writeByte(chars[i]->getLevel());
result.writeShort(chars[i]->getMoney());
}
}
break;
case PAMSG_LOGOUT:
{
result.writeShort(APMSG_LOGOUT_RESPONSE);
if ( computer.getAccount().get() == NULL )
{
LOG_INFO("Can't logout. Not even logged in.", 1);
result.writeByte(ERRMSG_NO_LOGIN);
}
else
{
LOG_INFO(computer.getAccount()->getName() << " logs out.", 1);
// computer.unsetCharacter(); Done by unsetAccount();
computer.unsetAccount();
result.writeByte(ERRMSG_OK);
}
}
break;
case PAMSG_REGISTER:
{
std::string clientVersion = message.readString();
std::string username = message.readString();
std::string password = message.readString();
std::string email = message.readString();
result.writeShort(APMSG_REGISTER_RESPONSE);
if (clientVersion < config.getValue("clientVersion", "0.0.0"))
{
LOG_INFO("Client has an unsufficient version number to login.", 1);
result.writeByte(REGISTER_INVALID_VERSION);
break;
}
// Checking if there are double quotes in it.
if (stringFilter->findDoubleQuotes(username))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(username << ": has got double quotes in it.", 1);
break;
}
// Checking conditions for having a good account.
LOG_INFO(username << " is trying to register.", 1);
// see if the account exists
tmwserv::AccountPtr accPtr = store.getAccount(username);
if ( accPtr.get() ) // Account already exists.
{
result.writeByte(REGISTER_EXISTS_USERNAME);
LOG_INFO(username << ": Username already exists.", 1);
}
else if ((username.length() < MIN_LOGIN_LENGTH) || (username.length() > MAX_LOGIN_LENGTH)) // Username length
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(username << ": Username too short or too long.", 1);
}
else if (!stringFilter->filterContent(username)) // Checking if the Name is slang's free.
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(username << ": has got bad words in it.", 1);
break;
}
else if ((password.length() < MIN_PASSWORD_LENGTH) || (password.length() > MAX_PASSWORD_LENGTH))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(email << ": Password too short or too long.", 1);
}
else if (!stringFilter->isEmailValid(email))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(email << ": Email Invalid, only a@b.c format is accepted.", 1);
}
else if (stringFilter->findDoubleQuotes(email))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(email << ": has got double quotes in it.", 1);
break;
}
else if (store.getSameEmailNumber(email) > 0) // Search if Email already exists.
{
result.writeByte(REGISTER_EXISTS_EMAIL);
LOG_INFO(email << ": Email already exists.", 1);
}
else
{
AccountPtr acc(new Account(username, password, email));
store.addAccount(acc);
result.writeByte(ERRMSG_OK);
store.flush(); // flush changes
LOG_INFO(username << ": Account registered.", 1);
}
}
break;
case PAMSG_UNREGISTER:
{
std::string username = message.readString();
std::string password = message.readString();
LOG_INFO(username << " wants to be deleted from our accounts.", 1);
result.writeShort(APMSG_UNREGISTER_RESPONSE);
if (stringFilter->findDoubleQuotes(username))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(username << ": has got double quotes in it.", 1);
break;
}
// see if the account exists
tmwserv::AccountPtr accPtr = store.getAccount(username);
if (!accPtr.get() || accPtr->getPassword() != password) {
LOG_INFO("Account does not exist of bad password for " << username << ".", 1);
result.writeByte(ERRMSG_INVALID_ARGUMENT);
} else {
// If the account to delete is the current account we're logged in.
// Get out of it in memory.
if (computer.getAccount().get() != NULL )
{
if (computer.getAccount()->getName() == username )
{
// computer.unsetCharacter(); Done by unsetAccount();
computer.unsetAccount();
}
}
// delete account and associated characters
LOG_INFO("Farewell " << username << " ...", 1);
store.delAccount(username);
store.flush();
result.writeByte(ERRMSG_OK);
}
}
break;
case PAMSG_EMAIL_CHANGE:
{
result.writeShort(APMSG_EMAIL_CHANGE_RESPONSE);
if (computer.getAccount().get() == NULL) {
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't change your Account's Email.", 1);
break;
}
std::string email = message.readString();
if (!stringFilter->isEmailValid(email))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(email << ": Invalid format, cannot change Email for " <<
computer.getAccount()->getName(), 1);
}
else if (stringFilter->findDoubleQuotes(email))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(email << ": has got double quotes in it.", 1);
}
else if (store.getSameEmailNumber(email) > 1) // Search if Email already exists,
{ // Except for the one already that is to
result.writeByte(EMAILCHG_EXISTS_EMAIL); // be changed.
LOG_INFO(email << ": New Email already exists.", 1);
}
else
{
computer.getAccount()->setEmail(email);
result.writeByte(ERRMSG_OK);
LOG_INFO(computer.getAccount()->getName() << ": Email changed to: " <<
email, 1);
}
}
break;
case PAMSG_EMAIL_GET:
{
result.writeShort(APMSG_EMAIL_GET_RESPONSE);
if (computer.getAccount().get() == NULL) {
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't get your Account's current Email.", 1);
break;
}
else
{
result.writeByte(ERRMSG_OK);
result.writeString(computer.getAccount()->getEmail());
}
}
break;
case PAMSG_PASSWORD_CHANGE:
{
result.writeShort(APMSG_PASSWORD_CHANGE_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't change your Account's Password.", 1);
break;
}
std::string oldPassword = message.readString();
std::string newPassword = message.readString();
if ( newPassword.length() < MIN_PASSWORD_LENGTH ||
newPassword.length() > MAX_PASSWORD_LENGTH )
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(computer.getAccount()->getName() <<
": New password too long or too short.", 1);
}
else if (stringFilter->findDoubleQuotes(newPassword))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(newPassword << ": has got double quotes in it.", 1);
}
else if ( oldPassword != computer.getAccount()->getPassword() )
{
result.writeByte(ERRMSG_FAILURE);
LOG_INFO(computer.getAccount()->getName() <<
": Old password is wrong.", 1);
}
else
{
computer.getAccount()->setPassword(newPassword);
result.writeByte(ERRMSG_OK);
LOG_INFO(computer.getAccount()->getName() <<
": The password was changed.", 1);
}
}
break;
case PAMSG_CHAR_CREATE:
{
result.writeShort(APMSG_CHAR_CREATE_RESPONSE);
if (computer.getAccount().get() == NULL) {
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't create a Character.", 1);
break;
}
// A player shouldn't have more than 3 characters.
tmwserv::Beings &chars = computer.getAccount()->getCharacters();
if (chars.size() >= MAX_OF_CHARACTERS)
{
result.writeByte(CREATE_TOO_MUCH_CHARACTERS);
LOG_INFO("Already has " << MAX_OF_CHARACTERS
<< " characters. Can't create another Character.", 1);
break;
}
std::string name = message.readString();
// Checking if the Name is slang's free.
if (!stringFilter->filterContent(name))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(name << ": Character has got bad words in it.", 1);
break;
}
// Checking if the Name has got double quotes.
if (stringFilter->findDoubleQuotes(name))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(name << ": has got double quotes in it.", 1);
break;
}
// Check if the character's name already exists
if (store.doesCharacterNameExists(name))
{
result.writeByte(CREATE_EXISTS_NAME);
LOG_INFO(name << ": Character's name already exists.", 1);
break;
}
// Check for character's name length
if ((name.length() < MIN_CHARACTER_LENGTH) || (name.length() > MAX_CHARACTER_LENGTH))
{
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO(name << ": Character's name too short or too long.", 1);
break;
}
char hairStyle = message.readByte();
if ((hairStyle < 0) || (hairStyle > (signed)MAX_HAIRSTYLE_VALUE))
{
result.writeByte(CREATE_INVALID_HAIRSTYLE);
LOG_INFO(name << ": Character's hair Style is invalid.", 1);
break;
}
char hairColor = message.readByte();
if ((hairColor < 0) || (hairColor > (signed)MAX_HAIRCOLOR_VALUE))
{
result.writeByte(CREATE_INVALID_HAIRCOLOR);
LOG_INFO(name << ": Character's hair Color is invalid.", 1);
break;
}
Genders gender = (Genders)message.readByte();
if ((gender < 0) || (gender > (signed)MAX_GENDER_VALUE))
{
result.writeByte(CREATE_INVALID_GENDER);
LOG_INFO(name << ": Character's gender is invalid.", 1);
break;
}
// LATER_ON: Add race, face and maybe special attributes.
// Customization of player's stats...
std::vector<unsigned short> rawStats;
rawStats.reserve(6);
// strength
rawStats.push_back((unsigned short)message.readShort());
// agility
rawStats.push_back((unsigned short)message.readShort());
// vitality
rawStats.push_back((unsigned short)message.readShort());
// intelligence
rawStats.push_back((unsigned short)message.readShort());
// dexterity
rawStats.push_back((unsigned short)message.readShort());
// luck
rawStats.push_back((unsigned short)message.readShort());
// We see if the difference between the lowest stat and the highest isn't too
// big.
unsigned short lowestStat = 0;
unsigned short highestStat = 0;
unsigned int totalStats = 0;
bool validNonZeroRawStats = true;
for ( std::vector<unsigned short>::iterator i = rawStats.begin(); i != rawStats.end();)
{
// For good total stat check.
totalStats = totalStats + *i;
// For checking if all stats are at least > 0
if (*i <= 0) validNonZeroRawStats = false;
if (lowestStat != 0)
{
if (lowestStat > *i) lowestStat = *i;
}
else
{
// We take the first value
lowestStat = *i;
}
if (highestStat != 0)
{
if (highestStat < *i) highestStat = *i;
}
else
{
// We take the first value
highestStat = *i;
}
++i;
}
if ( totalStats > POINTS_TO_DISTRIBUTES_AT_LVL1 )
{
result.writeByte(CREATE_RAW_STATS_TOO_HIGH);
LOG_INFO(name << ": Character's stats are too high to be of level 1.", 1);
break;
}
if ( totalStats < POINTS_TO_DISTRIBUTES_AT_LVL1 )
{
result.writeByte(CREATE_RAW_STATS_TOO_LOW);
LOG_INFO(name << ": Character's stats are too low to be of level 1.", 1);
break;
}
if ( (highestStat - lowestStat) > (signed)MAX_DIFF_BETWEEN_STATS )
{
result.writeByte(CREATE_RAW_STATS_INVALID_DIFF);
LOG_INFO(name << ": Character's stats difference is too high to be accepted.", 1);
break;
}
if ( !validNonZeroRawStats )
{
result.writeByte(CREATE_RAW_STATS_EQUAL_TO_ZERO);
LOG_INFO(name << ": One stat is equal to zero.", 1);
break;
}
// The reserve(6) method allows us to be sure that rawStats[5] will work.
tmwserv::RawStatistics stats = {rawStats[0], rawStats[1], rawStats[2],
rawStats[3], rawStats[4], rawStats[5]};
tmwserv::BeingPtr newCharacter(new tmwserv::Being(name, gender, hairStyle, hairColor,
1 /* level */, 0 /* Money */, stats));
newCharacter->setMapId((int)config.getValue("defaultMap", 1));
newCharacter->setXY((int)config.getValue("startX", 0),
(int)config.getValue("startY", 0));
computer.getAccount()->addCharacter(newCharacter);
LOG_INFO("Character " << name << " was created for "
<< computer.getAccount()->getName() << "'s account.", 1);
store.flush(); // flush changes
result.writeByte(ERRMSG_OK);
}
break;
case PAMSG_CHAR_SELECT:
{
result.writeShort(APMSG_CHAR_SELECT_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't select a Character.", 1);
break; // not logged in
}
unsigned char charNum = message.readByte();
tmwserv::Beings &chars = computer.getAccount()->getCharacters();
// Character ID = 0 to Number of Characters - 1.
if (charNum >= chars.size()) {
// invalid char selection
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO("Character Selection : Selection out of ID range.", 1);
break;
}
// set character
// TODO: Handle reset character's map when the server can't load
// it. And SELECT_NO_MAPS error return value when the default map couldn't
// be loaded in setCharacter(). Not implemented yet for tests purpose...
computer.setCharacter(chars[charNum]);
tmwserv::BeingPtr selectedChar = computer.getCharacter();
result.writeByte(ERRMSG_OK);
std::string mapName = store.getMapNameFromId(selectedChar->getMapId());
result.writeString(mapName);
result.writeShort(selectedChar->getX());
result.writeShort(selectedChar->getY());
LOG_INFO("Selected Character " << int(charNum)
<< ": " <<
selectedChar->getName(), 1);
}
break;
case PAMSG_CHAR_DELETE:
{
result.writeShort(APMSG_CHAR_DELETE_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't delete a Character.", 1);
break; // not logged in
}
unsigned char charNum = message.readByte();
tmwserv::Beings &chars = computer.getAccount()->getCharacters();
// Character ID = 0 to Number of Characters - 1.
if (charNum >= chars.size()) {
// invalid char selection
result.writeByte(ERRMSG_INVALID_ARGUMENT);
LOG_INFO("Character Deletion : Selection out of ID range.", 1);
break;
}
// Delete the character
// if the character to delete is the current character, get off of it in
// memory.
if ( computer.getCharacter().get() != NULL )
{
if ( computer.getCharacter()->getName() == chars[charNum].get()->getName() )
{
computer.unsetCharacter();
}
}
std::string deletedCharacter = chars[charNum].get()->getName();
computer.getAccount()->delCharacter(deletedCharacter);
store.flush();
LOG_INFO(deletedCharacter << ": Character deleted...", 1);
result.writeByte(ERRMSG_OK);
}
break;
case PAMSG_CHAR_LIST:
{
result.writeShort(APMSG_CHAR_LIST_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't list characters.", 1);
break; // not logged in
}
result.writeByte(ERRMSG_OK);
// Return information about available characters
tmwserv::Beings &chars = computer.getAccount()->getCharacters();
result.writeByte(chars.size());
LOG_INFO(computer.getAccount()->getName() << "'s account has "
<< chars.size() << " character(s).", 1);
std::string charStats;
std::string mapName;
for (unsigned int i = 0; i < chars.size(); i++)
{
result.writeString(chars[i]->getName());
if (i > 0) charStats += ", ";
charStats += chars[i]->getName();
result.writeByte(unsigned(short(chars[i]->getGender())));
result.writeByte(chars[i]->getHairStyle());
result.writeByte(chars[i]->getHairColor());
result.writeByte(chars[i]->getLevel());
result.writeShort(chars[i]->getMoney());
result.writeShort(chars[i]->getStrength());
result.writeShort(chars[i]->getAgility());
result.writeShort(chars[i]->getVitality());
result.writeShort(chars[i]->getIntelligence());
result.writeShort(chars[i]->getDexterity());
result.writeShort(chars[i]->getLuck());
mapName = store.getMapNameFromId(chars[i]->getMapId());
result.writeString(mapName);
result.writeShort(chars[i]->getX());
result.writeShort(chars[i]->getY());
}
charStats += ".";
LOG_INFO(charStats.c_str(), 1);
}
break;
case PAMSG_ENTER_WORLD:
{
result.writeShort(APMSG_ENTER_WORLD_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't enter the world.", 1);
break; // not logged in
}
if (computer.getCharacter().get() == NULL)
{
result.writeByte(ERRMSG_NO_CHARACTER_SELECTED);
LOG_INFO("No character selected. Can't enter the world.", 2);
break; // no character selected
}
std::string magic_token(32, ' ');
for(int i = 0; i < 32; ++i) magic_token[i] = 1 + (int) (127 * (rand() / (RAND_MAX + 1.0)));
result.writeByte(ERRMSG_OK);
result.writeString("localhost");
result.writeShort(9603);
result.writeString(magic_token, 32);
registerGameClient(magic_token, computer.getCharacter());
}
break;
case PAMSG_ENTER_CHAT:
{
result.writeShort(APMSG_ENTER_CHAT_RESPONSE);
if (computer.getAccount().get() == NULL)
{
result.writeByte(ERRMSG_NO_LOGIN);
LOG_INFO("Not logged in. Can't enter the chat.", 1);
break; // not logged in
}
if (computer.getCharacter().get() == NULL)
{
result.writeByte(ERRMSG_NO_CHARACTER_SELECTED);
LOG_INFO("No character selected. Can't enter the chat.", 2);
break; // no character selected
}
std::string magic_token(32, ' ');
for(int i = 0; i < 32; ++i) magic_token[i] = 1 + (int) (127 * (rand() / (RAND_MAX + 1.0)));
result.writeByte(ERRMSG_OK);
result.writeString("localhost");
result.writeShort(9603);
result.writeString(magic_token, 32);
registerChatClient(magic_token, computer.getCharacter()->getName(),
computer.getAccount()->getLevel());
}
break;
default:
LOG_WARN("Invalid message type", 0);
result.writeShort(XXMSG_INVALID);
break;
}
// return result
computer.send(result.getPacket());
}