예제 #1
0
void NpcHandler::handleMessage(Net::MessageIn &msg)
{
    if (msg.getId() == SMSG_NPC_CHOICE || msg.getId() == SMSG_NPC_MESSAGE)
    {
        msg.readInt16();  // length
    }

    int npcId = msg.readInt32();
    Event *event = 0;

    switch (msg.getId())
    {
    case SMSG_NPC_CHOICE:
        event = new Event(Event::Menu);
        event->setInt("id", npcId);
        parseMenu(event, msg.readString(msg.getLength() - 8));
        event->trigger(Event::NpcChannel);
        break;

    case SMSG_NPC_MESSAGE:
        event = new Event(Event::Message);
        event->setInt("id", npcId);
        event->setString("text", msg.readString(msg.getLength() - 8));
        event->trigger(Event::NpcChannel);
        break;

     case SMSG_NPC_CLOSE:
        // Show the close button
        event = new Event(Event::Close);
        event->setInt("id", npcId);
        event->trigger(Event::NpcChannel);
        break;

    case SMSG_NPC_NEXT:
        // Show the next button
        event = new Event(Event::Next);
        event->setInt("id", npcId);
        event->trigger(Event::NpcChannel);
        break;

    case SMSG_NPC_INT_INPUT:
        // Request for an integer
        event = new Event(Event::IntegerInput);
        event->setInt("id", npcId);
        event->trigger(Event::NpcChannel);
        break;

    case SMSG_NPC_STR_INPUT:
        // Request for a string
        event = new Event(Event::StringInput);
        event->setInt("id", npcId);
        event->trigger(Event::NpcChannel);
        break;
    }

    delete event;

    if (local_player->getCurrentAction() != Being::SIT)
        local_player->setAction(Being::STAND);
}
예제 #2
0
void CharServerHandler::processCharLogin(Net::MessageIn &msg)
{
    msg.skip(2);  // Length word
    const int slots = msg.readInt16();
    if (slots > 0 && slots < 30)
        loginData.characterSlots = static_cast<uint16_t>(slots);

    msg.skip(18);  // 0 Unused

    delete_all(mCharacters);
    mCharacters.clear();

    // Derive number of characters from message length
    const int count = (msg.getLength() - 24) / 106;

    for (int i = 0; i < count; ++i)
    {
        Net::Character *const character = new Net::Character;
        readPlayerData(msg, character, false);
        mCharacters.push_back(character);
        if (character->dummy)
        {
            logger->log("CharServer: Player: %s (%d)",
                character->dummy->getName().c_str(), character->slot);
        }
    }

    client->setState(STATE_CHAR_SELECT);
}
예제 #3
0
void BuySellHandler::processNpcSell(Net::MessageIn &msg)
{
    msg.readInt16("len");
    const int n_items = (msg.getLength() - 4) / 10;
    if (n_items > 0)
    {
        SellDialog *const dialog = new NpcSellDialog(mNpcId);
        dialog->postInit();
        dialog->setMoney(PlayerInfo::getAttribute(Attributes::MONEY));

        for (int k = 0; k < n_items; k++)
        {
            const int index = msg.readInt16("index") - INVENTORY_OFFSET;
            const int value = msg.readInt32("value");
            msg.readInt32("value?");

            const Item *const item = PlayerInfo::getInventory()
                ->getItem(index);

            if (item && item->isEquipped() == Equipped_false)
                dialog->addItem(item, value);
        }
    }
    else
    {
        NotifyManager::notify(NotifyTypes::SELL_LIST_EMPTY);
    }
}
예제 #4
0
void CharServerHandler::processCharLogin(Net::MessageIn &msg)
{
    msg.skip(2);  // Length word
    int slots = msg.readInt16();
    if (slots > 0 && slots < 30)
        loginData.characterSlots = static_cast<short unsigned int>(slots);

    bool version = msg.readInt8() == 1 && serverVersion > 0;
    msg.skip(17); // 0 Unused

    delete_all(mCharacters);
    mCharacters.clear();

    // Derive number of characters from message length
    int count = (msg.getLength() - 24);
    if (version)
        count /= 120;
    else
        count /= 106;

    for (int i = 0; i < count; ++i)
    {
        Net::Character *character = new Net::Character;
        readPlayerData(msg, character, version);
        mCharacters.push_back(character);
        if (character && character->dummy)
        {
            logger->log("CharServer: Player: %s (%d)",
                character->dummy->getName().c_str(), character->slot);
        }
    }

    Client::setState(STATE_CHAR_SELECT);
}
예제 #5
0
void CharServerHandler::processCharLogin(Net::MessageIn &msg)
{
    msg.skip(2, "packet len");
    const int slots = msg.readInt8("MAX_CHARS");
    msg.readInt8("sd->char_slots");
    msg.readInt8("MAX_CHARS");
    loginData.characterSlots = static_cast<uint16_t>(slots);

    msg.skip(20, "unused 0");

    delete_all(mCharacters);
    mCharacters.clear();

    // Derive number of characters from message length
    const int count = (msg.getLength() - 27)
        / (106 + 4 + 2 + 16 + 4 + 4 + 4 + 4);

    for (int i = 0; i < count; ++i)
    {
        Net::Character *const character = new Net::Character;
        readPlayerData(msg, character);
        mCharacters.push_back(character);
        if (character->dummy)
        {
            logger->log("CharServer: Player: %s (%d)",
                character->dummy->getName().c_str(), character->slot);
        }
    }

    client->setState(STATE_CHAR_SELECT);
}
예제 #6
0
void SkillHandler::processPlayerSkills(Net::MessageIn &msg) const
{
    msg.readInt16();  // length
    const int skillCount = (msg.getLength() - 4) / 37;
    int updateSkill = 0;

    for (int k = 0; k < skillCount; k++)
    {
        const int skillId = msg.readInt16();
        msg.readInt16();  // target type
        msg.skip(2);  // skill pool flags
        const int level = msg.readInt16();
        msg.readInt16();  // sp
        const int range = msg.readInt16();
        msg.skip(24);  // 0 unused
        const int up = msg.readInt8();
        const int oldLevel = PlayerInfo::getSkillLevel(skillId);
        if (oldLevel && oldLevel != level)
            updateSkill = skillId;
        PlayerInfo::setSkillLevel(skillId, level);
        if (skillDialog)
        {
            if (!skillDialog->updateSkill(skillId, range, up))
                skillDialog->addSkill(skillId, level, range, up);
        }
    }
    if (updateSkill && skillDialog)
        skillDialog->playUpdateEffect(updateSkill);
}
예제 #7
0
void BuySellHandler::processNpcSell(Net::MessageIn &msg, int offset)
{
    msg.readInt16();  // length
    int n_items = (msg.getLength() - 4) / 10;
    if (n_items > 0)
    {
        SellDialog *dialog = new SellDialog(mNpcId);
        dialog->setMoney(PlayerInfo::getAttribute(MONEY));

        for (int k = 0; k < n_items; k++)
        {
            int index = msg.readInt16() - offset;
            int value = msg.readInt32();
            msg.readInt32();  // OCvalue

            Item *item = PlayerInfo::getInventory()->getItem(index);

            if (item && !(item->isEquipped()))
                dialog->addItem(item, value);
        }
    }
    else
    {
        SERVER_NOTICE(_("Nothing to sell."))
    }
}
예제 #8
0
void SkillRecv::processPlayerSkills(Net::MessageIn &msg)
{
    msg.readInt16("len");
    const int sz = (serverVersion > 0) ? 41 : 37;
    const int skillCount = (msg.getLength() - 4) / sz;
    int updateSkill = 0;

    if (skillDialog != nullptr)
        skillDialog->hideSkills(SkillOwner::Player);
    for (int k = 0; k < skillCount; k++)
    {
        const int skillId = msg.readInt16("skill id");
        const SkillType::SkillType inf = static_cast<SkillType::SkillType>(
            msg.readInt32("inf"));
        if (serverVersion > 0)
            msg.readInt32("inf2");
        const int level = msg.readInt16("skill level");
        const int sp = msg.readInt16("sp");
        const int range = msg.readInt16("range");
        const std::string name = msg.readString(24, "skill name");
        const Modifiable up = fromBool(msg.readUInt8("up flag"), Modifiable);
        const int oldLevel = PlayerInfo::getSkillLevel(skillId);
        if ((oldLevel != 0) && oldLevel != level)
            updateSkill = skillId;
        PlayerInfo::setSkillLevel(skillId, level);
        if (skillDialog != nullptr)
        {
            if (!skillDialog->updateSkill(skillId, range, up, inf, sp))
            {
                skillDialog->addSkill(SkillOwner::Player,
                    skillId, name, level, range, up, inf, sp);
            }
        }
    }
    if (skillDialog != nullptr)
    {
        skillDialog->update();
        skillDialog->updateModelsHidden();
        if (updateSkill != 0)
            skillDialog->playUpdateEffect(updateSkill);
    }
}
예제 #9
0
void BuySellRecv::processNpcBuy(Net::MessageIn &msg)
{
    msg.readInt16("len");
    const unsigned int n_items = (msg.getLength() - 4U) / 11;
    CREATEWIDGETV(Ea::BuySellRecv::mBuyDialog, BuyDialog,
        Ea::BuySellRecv::mNpcId);
    Ea::BuySellRecv::mBuyDialog->setMoney(
        PlayerInfo::getAttribute(Attributes::MONEY));

    for (unsigned int k = 0; k < n_items; k++)
    {
        const int value = msg.readInt32("price");
        msg.readInt32("dc value?");
        const int type = msg.readUInt8("type");
        const int itemId = msg.readInt16("item id");
        const ItemColor color = ItemColor_one;
        Ea::BuySellRecv::mBuyDialog->addItem(itemId, type, color, 0, value);
    }
    Ea::BuySellRecv::mBuyDialog->sort();
}
예제 #10
0
void LoginHandler::processLoginData(Net::MessageIn &msg)
{
    // Skip the length word
    msg.skip(2);    // size

    clearWorlds();

    const int worldCount = (msg.getLength() - 47) / 32;

    mToken.session_ID1 = msg.readInt32();
    mToken.account_ID = msg.readInt32();
    mToken.session_ID2 = msg.readInt32();
    msg.skip(4);                           // old ip
    loginData.lastLogin = msg.readString(24);
    msg.skip(2);                           // 0 unused bytes

//    msg.skip(30);                           // unknown
    // reserve bits for future usage
    mToken.sex = Being::intToGender(static_cast<uint8_t>(
        msg.readUInt8() & 3U));

    for (int i = 0; i < worldCount; i++)
    {
        WorldInfo *const world = new WorldInfo;

        world->address = msg.readInt32();
        world->port = msg.readInt16();
        world->name = msg.readString(20);
        world->online_users = msg.readInt16();
        config.setValue("updatehost", mUpdateHost);
        world->updateHost = mUpdateHost;
        msg.skip(2);                        // maintenance
        msg.skip(2);                        // new

        logger->log("Network: Server: %s (%s:%d)", world->name.c_str(),
            ipToString(world->address), world->port);

        mWorlds.push_back(world);
    }
    client->setState(STATE_WORLD_SELECT);
}
예제 #11
0
void BuySellHandler::processNpcBuy(Net::MessageIn &msg)
{
    msg.readInt16();  // length
    int sz = 11;
    if (serverVersion > 0)
        sz += 1;
    const int n_items = (msg.getLength() - 4) / sz;
    mBuyDialog = new BuyDialog(mNpcId);
    mBuyDialog->setMoney(PlayerInfo::getAttribute(PlayerInfo::MONEY));

    for (int k = 0; k < n_items; k++)
    {
        const int value = msg.readInt32();
        msg.readInt32();  // DCvalue
        msg.readInt8();  // type
        const int itemId = msg.readInt16();
        unsigned char color = 1;
        if (serverVersion > 0)
            color = msg.readInt8();
        mBuyDialog->addItem(itemId, color, 0, value);
    }
}
예제 #12
0
void SkillRecv::processPlayerSkills(Net::MessageIn &msg)
{
    msg.readInt16("len");
    const int skillCount = (msg.getLength() - 4) / 37;
    int updateSkill = 0;

    for (int k = 0; k < skillCount; k++)
    {
        const int skillId = msg.readInt16("skill id");
        const SkillType::SkillType inf = static_cast<SkillType::SkillType>(
            msg.readInt16("inf"));
        msg.readInt16("skill pool flags");
        const int level = msg.readInt16("skill level");
        const int sp = msg.readInt16("sp");
        const int range = msg.readInt16("range");
        msg.skip(24, "unused");
        const Modifiable up = fromBool(msg.readUInt8("up flag"), Modifiable);
        const int oldLevel = PlayerInfo::getSkillLevel(skillId);
        if (oldLevel && oldLevel != level)
            updateSkill = skillId;
        PlayerInfo::setSkillLevel(skillId, level);
        if (skillDialog)
        {
            if (!skillDialog->updateSkill(skillId, range, up, inf, sp))
            {
                skillDialog->addSkill(SkillOwner::Player,
                    skillId, "", level, range, up, inf, sp);
            }
        }
    }
    if (skillDialog)
    {
        skillDialog->update();
        if (updateSkill)
            skillDialog->playUpdateEffect(updateSkill);
    }
}
예제 #13
0
void SpecialHandler::handleMessage(Net::MessageIn &msg)
{
    int skillCount;
    int skillId;

    switch (msg.getId())
    {
        case SMSG_PLAYER_SKILLS:
            msg.readInt16();  // length
            skillCount = (msg.getLength() - 4) / 37;

            for (int k = 0; k < skillCount; k++)
            {
                skillId = msg.readInt16();
                msg.readInt16();  // target type
                msg.skip(2);  // unused
                int level = msg.readInt16();
                msg.readInt16(); // sp
                msg.readInt16(); // range
                msg.skip(24); // unused
                int up = msg.readInt8();

                PlayerInfo::setStatBase(skillId, level);
                if (skillDialog)
                    skillDialog->setModifiable(skillId, up);
            }
            break;

        case SMSG_PLAYER_SKILL_UP:
            {
                skillId = msg.readInt16();
                int level = msg.readInt16();
                msg.readInt16(); // sp
                msg.readInt16(); // range
                int up = msg.readInt8();

                PlayerInfo::setStatBase(skillId, level);
                skillDialog->setModifiable(skillId, up);
            }
            break;

        case SMSG_SKILL_FAILED:
            // Action failed (ex. sit because you have not reached the
            // right level)
            skillId   = msg.readInt16();
            short bskill  = msg.readInt16();
            msg.readInt16(); // unknown
            char success = msg.readInt8();
            char reason  = msg.readInt8();
            if (success != SKILL_FAILED && bskill == BSKILL_EMOTE)
            {
                logger->log("Action: %d/%d", bskill, success);
            }

            std::string msg;
            if (success == SKILL_FAILED && skillId == SKILL_BASIC)
            {
                switch (bskill)
                {
                    case BSKILL_TRADE:
                        msg = _("Trade failed!");
                        break;
                    case BSKILL_EMOTE:
                        msg = _("Emote failed!");
                        break;
                    case BSKILL_SIT:
                        msg = _("Sit failed!");
                        break;
                    case BSKILL_CREATECHAT:
                        msg = _("Chat creating failed!");
                        break;
                    case BSKILL_JOINPARTY:
                        msg = _("Could not join party!");
                        break;
                    case BSKILL_SHOUT:
                        msg = _("Cannot shout!");
                        break;
                }

                msg += " ";

                switch (reason)
                {
                    case RFAIL_SKILLDEP:
                        msg += _("You have not yet reached a high enough lvl!");
                        break;
                    case RFAIL_INSUFHP:
                        msg += _("Insufficient HP!");
                        break;
                    case RFAIL_INSUFSP:
                        msg += _("Insufficient SP!");
                        break;
                    case RFAIL_NOMEMO:
                        msg += _("You have no memos!");
                        break;
                    case RFAIL_SKILLDELAY:
                        msg += _("You cannot do that right now!");
                        break;
                    case RFAIL_ZENY:
                        msg += _("Seems you need more money... ;-)");
                        break;
                    case RFAIL_WEAPON:
                        msg += _("You cannot use this skill with that kind of weapon!");
                        break;
                    case RFAIL_REDGEM:
                        msg += _("You need another red gem!");
                        break;
                    case RFAIL_BLUEGEM:
                        msg += _("You need another blue gem!");
                        break;
                    case RFAIL_OVERWEIGHT:
                        msg += _("You're carrying to much to do this!");
                        break;
                    default:
                        msg += _("Huh? What's that?");
                        break;
                }
            }
            else
            {
                switch (skillId)
                {
                    case SKILL_WARP :
                        msg = _("Warp failed...");
                        break;
                    case SKILL_STEAL :
                        msg = _("Could not steal anything...");
                        break;
                    case SKILL_ENVENOM :
                        msg = _("Poison had no effect...");
                        break;
                }
            }

            SERVER_NOTICE(msg)
            break;
    }
}
예제 #14
0
void LoginHandler::handleMessage(Net::MessageIn &msg)
{
    int code, worldCount;

    switch (msg.getId())
    {
        case SMSG_CHAR_PASSWORD_RESPONSE:
        {
            // 0: acc not found, 1: success, 2: password mismatch, 3: pass too short
            int errMsg = msg.readInt8();
            // Successful pass change
            if (errMsg == 1)
            {
                Client::setState(STATE_CHANGEPASSWORD_SUCCESS);
            }
            // pass change failed
            else
            {
                switch (errMsg)
                {
                    case 0:
                        errorMessage = _("Account was not found. Please re-login.");
                        break;
                    case 2:
                        errorMessage = _("Old password incorrect.");
                        break;
                    case 3:
                        errorMessage = _("New password too short.");
                        break;
                    default:
                        errorMessage = _("Unknown error.");
                        break;
                }
                Client::setState(STATE_ACCOUNTCHANGE_ERROR);
            }
        }
            break;

        case SMSG_UPDATE_HOST:
             int len;

             len = msg.readInt16() - 4;
             mUpdateHost = msg.readString(len);
             loginData.updateHost = mUpdateHost;

             logger->log("Received update host \"%s\" from login server.",
                     mUpdateHost.c_str());
             break;

        case SMSG_LOGIN_DATA:
            // Skip the length word
            msg.skip(2);

            clearWorlds();

            worldCount = (msg.getLength() - 47) / 32;

            mToken.session_ID1 = msg.readInt32();
            mToken.account_ID = msg.readInt32();
            mToken.session_ID2 = msg.readInt32();
            msg.skip(30);                           // unknown
            mToken.sex = msg.readInt8() ? GENDER_MALE : GENDER_FEMALE;

            for (int i = 0; i < worldCount; i++)
            {
                WorldInfo *world = new WorldInfo;

                world->address = msg.readInt32();
                world->port = msg.readInt16();
                world->name = msg.readString(20);
                world->online_users = msg.readInt32();
                world->updateHost = mUpdateHost;
                msg.skip(2);                        // unknown

                logger->log("Network: Server: %s (%s:%d)",
                        world->name.c_str(),
                        ipToString(world->address),
                        world->port);

                mWorlds.push_back(world);
            }
            Client::setState(STATE_WORLD_SELECT);
            break;

        case SMSG_LOGIN_ERROR:
            code = msg.readInt8();
            logger->log("Login::error code: %i", code);

            switch (code)
            {
                case 0:
                    errorMessage = _("Unregistered ID.");
                    break;
                case 1:
                    errorMessage = _("Wrong password.");
                    break;
                case 2:
                    errorMessage = _("Account expired.");
                    break;
                case 3:
                    errorMessage = _("Rejected from server.");
                    break;
                case 4:
                    errorMessage = _("You have been permanently banned from "
                                     "the game. Please contact the GM team.");
                    break;
                case 5:
                    errorMessage = _("Client too old.");
                    break;
                case 6:
                    errorMessage = strprintf(_("You have been temporarily "
                                               "banned from the game until "
                                               "%s.\nPlease contact the GM "
                                               "team via the forums."),
                                               msg.readString(20).c_str());
                    break;
                case 7:
                    errorMessage = _("Server overpopulated.");
                    break;
                case 9:
                    errorMessage = _("This user name is already taken.");
                    break;
                case 99:
                    errorMessage = _("Username permanently erased.");
                    break;
                default:
                    errorMessage = _("Unknown error.");
                    break;
            }
            Client::setState(STATE_ERROR);
            break;

        case SMSG_SERVER_VERSION_RESPONSE:
            {
                // TODO: verify these!

                msg.readInt8(); // -1
                msg.readInt8(); // T
                msg.readInt8(); // M
                msg.readInt8(); // W

                unsigned int options = msg.readInt32();

                mRegistrationEnabled = (options & 1);

                // Leave this last
                mVersionResponse = true;
            }
            break;
    }
}
예제 #15
0
void CharServerHandler::handleMessage(Net::MessageIn &msg)
{
    switch (msg.getId())
    {
            case SMSG_CHAR_LOGIN:
            {
                msg.skip(2);  // Length word
                msg.skip(20); // Unused

                // Derive number of characters from message length
                const int count = (msg.getLength() - 24) / 106;

                for (int i = 0; i < count; ++i)
                {
                    Net::Character *character = new Net::Character;
                    readPlayerData(msg, character);
                    mCharacters.push_back(character);
                    logger->log("CharServer: Player: %s (%d)",
                                character->dummy->getName().c_str(), character->slot);
                }

                Client::setState(STATE_CHAR_SELECT);
            }
            break;

        case SMSG_CHAR_LOGIN_ERROR:
            switch (msg.readInt8())
            {
                case 0:
                    errorMessage = _("Access denied. Most likely, there are "
                                     "too many players on this server.");
                    break;
                case 1:
                    errorMessage = _("Cannot use this ID.");
                    break;
                default:
                    errorMessage = _("Unknown char-server failure.");
                    break;
            }
            Client::setState(STATE_ERROR);
            break;

        case SMSG_CHAR_CREATE_SUCCEEDED:
            {
                Net::Character *character = new Net::Character;
                readPlayerData(msg, character);
                mCharacters.push_back(character);

                updateCharSelectDialog();

                // Close the character create dialog
                if (mCharCreateDialog)
                {
                    mCharCreateDialog->scheduleDelete();
                    mCharCreateDialog = 0;
                }
            }
            break;

        case SMSG_CHAR_CREATE_FAILED:
            new OkDialog(_("Error"), _("Failed to create character. Most "
                                       "likely the name is already taken."));
            if (mCharCreateDialog)
                mCharCreateDialog->unlock();
            break;

        case SMSG_CHAR_DELETE_SUCCEEDED:
            delete mSelectedCharacter;
            mCharacters.remove(mSelectedCharacter);
            mSelectedCharacter = 0;
            updateCharSelectDialog();
            unlockCharSelectDialog();
            new OkDialog(_("Info"), _("Character deleted."));
            break;

        case SMSG_CHAR_DELETE_FAILED:
            unlockCharSelectDialog();
            new OkDialog(_("Error"), _("Failed to delete character."));
            break;

        case SMSG_CHAR_MAP_INFO:
        {
            msg.skip(4); // CharID, must be the same as player_node->charID
            GameHandler *gh = static_cast<GameHandler*>(Net::getGameHandler());
            gh->setMap(msg.readString(16));
            mapServer.hostname = ipToString(msg.readInt32());
            mapServer.port = msg.readInt16();

            // Prevent the selected local player from being deleted
            player_node = mSelectedCharacter->dummy;
            PlayerInfo::setBackend(mSelectedCharacter->data);

            mSelectedCharacter->dummy = 0;

            delete_all(mCharacters);
            mCharacters.clear();
            updateCharSelectDialog();

            mNetwork->disconnect();
            Client::setState(STATE_CONNECT_GAME);
        }
        break;

        case SMSG_CHANGE_MAP_SERVER:
        {
            GameHandler *gh = static_cast<GameHandler*>(Net::getGameHandler());
            gh->setMap(msg.readString(16));
            int x = msg.readInt16();
            int y = msg.readInt16();
            mapServer.hostname = ipToString(msg.readInt32());
            mapServer.port = msg.readInt16();

            mNetwork->disconnect();
            Client::setState(STATE_CHANGE_MAP);
            player_node->setTileCoords(x, y);
            player_node->setMap(0);
        }
        break;
    }
}
예제 #16
0
void LoginRecv::processLoginData(Net::MessageIn &msg)
{
    msg.readInt16("len");

    loginHandler->clearWorlds();

    int offset = 0;
    int serverLen = 0;
    if (msg.getVersion() >= 20170315)
    {
        offset = 47 + 17;
        serverLen = 32 + 128;
    }
    else
    {
        offset = 47;
        serverLen = 32;
    }

    const int worldCount = (msg.getLength() - offset) / serverLen;

    Ea::LoginRecv::mToken.session_ID1 = msg.readInt32("session id1");
    Ea::LoginRecv::mToken.account_ID = msg.readBeingId("accound id");
    Ea::LoginRecv::mToken.session_ID2 = msg.readInt32("session id2");
    msg.readInt32("old ip");
    loginData.lastLogin = msg.readString(24, "last login");
    msg.readInt16("unused");

    // reserve bits for future usage
    Ea::LoginRecv::mToken.sex = Being::intToGender(CAST_U8(
        msg.readUInt8("gender") & 3U));

    if (msg.getVersion() >= 20170315)
    {
        msg.readString(16, "twitter auth token");
        msg.readUInt8("twitter flag");
    }

    for (int i = 0; i < worldCount; i++)
    {
        WorldInfo *const world = new WorldInfo;

        world->address = msg.readInt32("ip address");
        world->port = msg.readInt16("port");
        world->name = msg.readString(20, "name");
        world->online_users = msg.readInt16("online number");
        config.setValue("updatehost", Ea::LoginRecv::mUpdateHost);
        world->updateHost = Ea::LoginRecv::mUpdateHost;
        msg.readInt16("maintenance");
        msg.readInt16("new");
        if (msg.getVersion() >= 20170315)
        {
            for (int f = 0; f < 32; f ++)
                msg.readInt32("unused2");
        }

        logger->log("Network: Server: %s (%s:%d)", world->name.c_str(),
            ipToString(world->address), world->port);

        Ea::LoginRecv::mWorlds.push_back(world);
    }
    client->setState(State::WORLD_SELECT);
}