/** * The Forgotten Server - a free and open-source MMORPG server emulator * Copyright (C) 2019 Mark Samman * * 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "otpch.h" #include #include "protocolgame.h" #include "outputmessage.h" #include "player.h" #include "actions.h" #include "ban.h" #include "configmanager.h" #include "game.h" #include "iologindata.h" #include "iomarket.h" #include "scheduler.h" #include "waitlist.h" extern ConfigManager g_config; extern Actions actions; extern CreatureEvents *g_creatureEvents; extern Chat *g_chat; void ProtocolGame::release() { // dispatcher thread if (player && player->client == shared_from_this()) { player->client.reset(); player->decrementReferenceCounter(); player = nullptr; } OutputMessagePool::getInstance().removeProtocolFromAutosend( shared_from_this()); Protocol::release(); } void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingSystem_t operatingSystem) { // OTCv8 features and extended opcodes if (otclientV8 || operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { if (otclientV8) sendFeatures(); NetworkMessage opcodeMessage; opcodeMessage.addByte(0x32); opcodeMessage.addByte(0x00); opcodeMessage.add(0x00); writeToOutputBuffer(opcodeMessage); } // dispatcher thread Player *foundPlayer = g_game.getPlayerByName(name); if (!foundPlayer || g_config.getBoolean(ConfigManager::ALLOW_CLONES)) { player = new Player(getThis()); player->setName(name); player->incrementReferenceCounter(); player->setID(); if (!IOLoginData::preloadPlayer(player, name)) { disconnectClient("Your character could not be loaded."); return; } if (IOBan::isPlayerNamelocked(player->getGUID())) { disconnectClient("Your character has been namelocked."); return; } if (g_game.getGameState() == GAME_STATE_CLOSING && !player->hasFlag(PlayerFlag_CanAlwaysLogin)) { disconnectClient("The game is just going down.\nPlease try again later."); return; } if (g_game.getGameState() == GAME_STATE_CLOSED && !player->hasFlag(PlayerFlag_CanAlwaysLogin)) { disconnectClient("Server is currently closed.\nPlease try again later."); return; } if (g_config.getBoolean(ConfigManager::ONE_PLAYER_ON_ACCOUNT) && player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && g_game.getPlayerByAccount(player->getAccount())) { disconnectClient("You may only login with one character\nof your account " "at the same time."); return; } if (!player->hasFlag(PlayerFlag_CannotBeBanned)) { BanInfo banInfo; if (IOBan::isAccountBanned(accountId, banInfo)) { if (banInfo.reason.empty()) { banInfo.reason = "(none)"; } std::ostringstream ss; if (banInfo.expiresAt > 0) { ss << "Your account has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; } else { ss << "Your account has been permanently banned by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; } disconnectClient(ss.str()); return; } } std::size_t currentSlot = WaitingList::getInstance().clientLogin(player); if (currentSlot > 0) { uint8_t retryTime = WaitingList::getTime(currentSlot); std::ostringstream ss; ss << "Too many players online.\nYou are at place " << currentSlot << " on the waiting list."; auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x16); output->addString(ss.str()); output->addByte(retryTime); send(output); disconnect(); return; } if (!IOLoginData::loadPlayerById(player, player->getGUID())) { disconnectClient("Your character could not be loaded."); return; } player->setOperatingSystem(operatingSystem); if (!g_game.placeCreature(player, player->getLoginPosition())) { if (!g_game.placeCreature(player, player->getTemplePosition(), false, true)) { disconnectClient( "Temple position is wrong. Contact the administrator."); return; } } if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { player->registerCreatureEvent("ExtendedOpcode"); } player->lastIP = player->getIP(); player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); acceptPackets = true; } else { if (eventConnect != 0 || !g_config.getBoolean(ConfigManager::REPLACE_KICK_ON_LOGIN)) { // Already trying to connect disconnectClient("You are already logged in."); return; } if (foundPlayer->client) { foundPlayer->disconnect(); foundPlayer->isConnecting = true; eventConnect = g_scheduler.addEvent(createSchedulerTask( 1000, std::bind(&ProtocolGame::connect, getThis(), foundPlayer->getID(), operatingSystem))); } else { connect(foundPlayer->getID(), operatingSystem); } } OutputMessagePool::getInstance().addProtocolToAutosend(shared_from_this()); } void ProtocolGame::connect(uint32_t playerId, OperatingSystem_t operatingSystem) { eventConnect = 0; Player *foundPlayer = g_game.getPlayerByID(playerId); if (!foundPlayer || foundPlayer->client) { disconnectClient("You are already logged in."); return; } if (isConnectionExpired()) { // ProtocolGame::release() has been called at this point and the Connection // object no longer exists, so we return to prevent leakage of the Player. return; } player = foundPlayer; player->incrementReferenceCounter(); g_chat->removeUserFromAllChannels(*player); player->clearModalWindows(); player->setOperatingSystem(operatingSystem); player->isConnecting = false; player->client = getThis(); sendAddCreature(player, player->getPosition(), 0, false); player->lastIP = player->getIP(); player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); acceptPackets = true; } void ProtocolGame::logout(bool displayEffect, bool forced) { // dispatcher thread if (!player) { return; } if (!player->isRemoved()) { if (!forced) { if (!player->isAccessPlayer()) { if (player->getTile()->hasFlag(TILESTATE_NOLOGOUT)) { player->sendCancelMessage(RETURNVALUE_YOUCANNOTLOGOUTHERE); return; } if (!player->getTile()->hasFlag(TILESTATE_PROTECTIONZONE) && player->hasCondition(CONDITION_INFIGHT)) { player->sendCancelMessage(RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT); return; } } // scripting event - onLogout if (!g_creatureEvents->playerLogout(player)) { // Let the script handle the error message return; } } if (displayEffect && player->getHealth() > 0) { g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); } } disconnect(); g_game.removeCreature(player); } void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) { if (g_game.getGameState() == GAME_STATE_SHUTDOWN) { disconnect(); return; } OperatingSystem_t operatingSystem = static_cast(msg.get()); version = msg.get(); msg.skipBytes(7); // U32 client version, U8 client type, U16 dat revision if (!Protocol::RSA_decrypt(msg)) { disconnect(); return; } xtea::key key; key[0] = msg.get(); key[1] = msg.get(); key[2] = msg.get(); key[3] = msg.get(); enableXTEAEncryption(); setXTEAKey(std::move(key)); msg.skipBytes(1); // gamemaster flag std::string sessionKey = msg.getString(); auto sessionArgs = explodeString(sessionKey, "\n", 4); if (sessionArgs.size() != 4) { disconnect(); return; } std::string &accountName = sessionArgs[0]; std::string &password = sessionArgs[1]; std::string &token = sessionArgs[2]; uint32_t tokenTime = 0; try { tokenTime = std::stoul(sessionArgs[3]); } catch (const std::invalid_argument &) { disconnectClient("Malformed token packet."); return; } catch (const std::out_of_range &) { disconnectClient("Token time is too long."); return; } if (accountName.empty()) { disconnectClient("You must enter your account name."); return; } std::string characterName = msg.getString(); uint32_t timeStamp = msg.get(); uint8_t randNumber = msg.getByte(); if (challengeTimestamp != timeStamp || challengeRandom != randNumber) { disconnect(); return; } // OTCv8 version detection uint16_t otcV8StringLength = msg.get(); if (otcV8StringLength == 5 && msg.getString(5) == "OTCv8") { otclientV8 = msg.get(); // 253, 260, 261, ... } if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) { std::ostringstream ss; ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; disconnectClient(ss.str()); return; } if (g_game.getGameState() == GAME_STATE_STARTUP) { disconnectClient("Gameworld is starting up. Please wait."); return; } if (g_game.getGameState() == GAME_STATE_MAINTAIN) { disconnectClient( "Gameworld is under maintenance. Please re-connect in a while."); return; } BanInfo banInfo; if (IOBan::isIpBanned(getIP(), banInfo)) { if (banInfo.reason.empty()) { banInfo.reason = "(none)"; } std::ostringstream ss; ss << "Your IP has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; disconnectClient(ss.str()); return; } uint32_t accountId = IOLoginData::gameworldAuthentication( accountName, password, characterName, token, tokenTime); if (accountId == 0) { disconnectClient("Account name or password is not correct."); return; } g_dispatcher.addTask( createTask(std::bind(&ProtocolGame::login, getThis(), characterName, accountId, operatingSystem))); } void ProtocolGame::onConnect() { auto output = OutputMessagePool::getOutputMessage(); static std::random_device rd; static std::ranlux24 generator(rd()); static std::uniform_int_distribution randNumber(0x00, 0xFF); // Skip checksum output->skipBytes(sizeof(uint32_t)); // Packet length & type output->add(0x0006); output->addByte(0x1F); // Add timestamp & random number challengeTimestamp = static_cast(time(nullptr)); output->add(challengeTimestamp); challengeRandom = randNumber(generator); output->addByte(challengeRandom); // Go back and write checksum output->skipBytes(-12); output->add( adlerChecksum(output->getOutputBuffer() + sizeof(uint32_t), 8)); send(output); } void ProtocolGame::disconnectClient(const std::string &message) const { auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x14); output->addString(message); send(output); disconnect(); } void ProtocolGame::writeToOutputBuffer(const NetworkMessage &msg) { auto out = getOutputBuffer(msg.getLength()); out->append(msg); } void ProtocolGame::parsePacket(NetworkMessage &msg) { if (!acceptPackets || g_game.getGameState() == GAME_STATE_SHUTDOWN || msg.getLength() <= 0) { return; } uint8_t recvbyte = msg.getByte(); if (!player) { if (recvbyte == 0x0F) { disconnect(); } return; } // a dead player can not performs actions if (player->isRemoved() || player->getHealth() <= 0) { if (recvbyte == 0x0F) { disconnect(); return; } if (recvbyte != 0x14) { return; } } switch (recvbyte) { case 0x14: g_dispatcher.addTask( createTask(std::bind(&ProtocolGame::logout, getThis(), true, false))); break; case 0x1D: addGameTask(&Game::playerReceivePingBack, player->getID()); break; case 0x1E: addGameTask(&Game::playerReceivePing, player->getID()); break; case 0x32: parseExtendedOpcode(msg); break; // otclient extended opcode case 0x40: parseNewPing(msg); break; case 0x42: parseChangeAwareRange(msg); break; case 0x45: parseNewWalking(msg); break; case 0x64: parseAutoWalk(msg); break; case 0x65: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTH); break; case 0x66: addGameTask(&Game::playerMove, player->getID(), DIRECTION_EAST); break; case 0x67: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTH); break; case 0x68: addGameTask(&Game::playerMove, player->getID(), DIRECTION_WEST); break; case 0x69: addGameTask(&Game::playerStopAutoWalk, player->getID()); break; case 0x6A: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTHEAST); break; case 0x6B: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTHEAST); break; case 0x6C: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTHWEST); break; case 0x6D: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTHWEST); break; case 0x6F: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_NORTH); break; case 0x70: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_EAST); break; case 0x71: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_SOUTH); break; case 0x72: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_WEST); break; case 0x77: parseEquipObject(msg); break; case 0x78: parseThrow(msg); break; case 0x79: parseLookInShop(msg); break; case 0x7A: parsePlayerPurchase(msg); break; case 0x7B: parsePlayerSale(msg); break; case 0x7C: addGameTask(&Game::playerCloseShop, player->getID()); break; case 0x7D: parseRequestTrade(msg); break; case 0x7E: parseLookInTrade(msg); break; case 0x7F: addGameTask(&Game::playerAcceptTrade, player->getID()); break; case 0x80: addGameTask(&Game::playerCloseTrade, player->getID()); break; case 0x82: parseUseItem(msg); break; case 0x83: parseUseItemEx(msg); break; case 0x84: parseUseWithCreature(msg); break; case 0x85: parseRotateItem(msg); break; case 0x87: parseCloseContainer(msg); break; case 0x88: parseUpArrowContainer(msg); break; case 0x89: parseTextWindow(msg); break; case 0x8A: parseHouseWindow(msg); break; case 0x8B: parseWrapItem(msg); break; case 0x8C: parseLookAt(msg); break; case 0x8D: parseLookInBattleList(msg); break; case 0x8E: /* join aggression */ break; case 0x96: parseSay(msg); break; case 0x97: addGameTask(&Game::playerRequestChannels, player->getID()); break; case 0x98: parseOpenChannel(msg); break; case 0x99: parseCloseChannel(msg); break; case 0x9A: parseOpenPrivateChannel(msg); break; case 0x9E: addGameTask(&Game::playerCloseNpcChannel, player->getID()); break; case 0xA0: parseFightModes(msg); break; case 0xA1: parseAttack(msg); break; case 0xA2: parseFollow(msg); break; case 0xA3: parseInviteToParty(msg); break; case 0xA4: parseJoinParty(msg); break; case 0xA5: parseRevokePartyInvite(msg); break; case 0xA6: parsePassPartyLeadership(msg); break; case 0xA7: addGameTask(&Game::playerLeaveParty, player->getID()); break; case 0xA8: parseEnableSharedPartyExperience(msg); break; case 0xAA: addGameTask(&Game::playerCreatePrivateChannel, player->getID()); break; case 0xAB: parseChannelInvite(msg); break; case 0xAC: parseChannelExclude(msg); break; case 0xBE: addGameTask(&Game::playerCancelAttackAndFollow, player->getID()); break; case 0xC9: /* update tile */ break; case 0xCA: parseUpdateContainer(msg); break; case 0xCB: parseBrowseField(msg); break; case 0xCC: parseSeekInContainer(msg); break; case 0xD2: addGameTask(&Game::playerRequestOutfit, player->getID()); break; case 0xD3: parseSetOutfit(msg); break; case 0xD4: parseToggleMount(msg); break; case 0xDC: parseAddVip(msg); break; case 0xDD: parseRemoveVip(msg); break; case 0xDE: parseEditVip(msg); break; case 0xE6: parseBugReport(msg); break; case 0xE7: /* thank you */ break; case 0xE8: parseDebugAssert(msg); break; case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; case 0xF1: parseQuestLine(msg); break; case 0xF2: parseRuleViolationReport(msg); break; case 0xF3: /* get object info */ break; case 0xF4: parseMarketLeave(); break; case 0xF5: parseMarketBrowse(msg); break; case 0xF6: parseMarketCreateOffer(msg); break; case 0xF7: parseMarketCancelOffer(msg); break; case 0xF8: parseMarketAcceptOffer(msg); break; case 0xF9: parseModalWindowAnswer(msg); break; default: // std::cout << "Player: " << player->getName() << " sent an unknown packet // header: 0x" << std::hex << static_cast(recvbyte) << std::dec << // "!" << std::endl; break; } if (msg.isOverrun()) { disconnect(); } } void ProtocolGame::GetTileDescription(const Tile *tile, NetworkMessage &msg) { Item *ground = tile->getGround(); if (otclientV8) { // OTCLIENTV8 new walking uint16_t groundSpeed = 150; // I've noticed that sometimes ground speed was incorrect in otclient, // so server just sends 100% correct ground speed bool isBlocking = false; // if isBlocking is true, next prewalking from this tile will be impossible // till server confirmation/rejection, used for teleport or stairs if (ground) { groundSpeed = (uint16_t)Item::items[ground->getID()].speed; if (groundSpeed == 0) { groundSpeed = 150; } // you can uncomment to make walk animation slower when going into stairs, // looks cool floor change speed is the same, only animation is slower // if(tile->hasFlag(TILESTATE_FLOORCHANGE)) // groundSpeed *= 3; if (tile->hasFlag(TILESTATE_FLOORCHANGE) || tile->hasFlag(TILESTATE_TELEPORT)) isBlocking = true; } msg.add(groundSpeed); msg.add(isBlocking ? 1 : 0); } else { msg.add(0x00); // environmental effects } int32_t count; if (ground) { msg.addItem(ground); count = 1; } else { count = 0; } const TileItemVector *items = tile->getItemList(); if (items) { for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { msg.addItem(*it); count++; if (count == 9 && tile->getPosition() == player->getPosition() && !otclientV8) { break; } else if (count == 10) { return; } } } const CreatureVector *creatures = tile->getCreatures(); if (creatures) { bool playerAdded = false; for (const Creature *creature : boost::adaptors::reverse(*creatures)) { if (!player->canSeeCreature(creature)) { continue; } if (tile->getPosition() == player->getPosition() && count == 9 && !playerAdded && !otclientV8) { creature = player; } if (creature->getID() == player->getID()) { playerAdded = true; } bool known; uint32_t removedKnown; checkCreatureAsKnown(creature->getID(), known, removedKnown); AddCreature(msg, creature, known, removedKnown); if (++count == 10) { return; } } } if (items) { for (auto it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { msg.addItem(*it); if (++count == 10) { return; } } } } void ProtocolGame::GetMapDescription(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, NetworkMessage &msg) { int32_t skip = -1; int32_t startz, endz, zstep; if (z > 7) { startz = z - 5; endz = std::min(MAP_MAX_LAYERS - 1, z + 5); zstep = 1; } else { // FIX: send ALL floors (15->0) for surface players so they can see // the entire underground map through holes, cave entrances, etc. startz = MAP_MAX_LAYERS - 1; // = 15 endz = 0; zstep = -1; } for (int32_t nz = startz; nz != endz + zstep; nz += zstep) { GetFloorDescription(msg, x, y, nz, width, height, z - nz, skip); } if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } } void ProtocolGame::GetFloorDescription(NetworkMessage &msg, int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, int32_t offset, int32_t &skip) { for (int32_t nx = 0; nx < width; nx++) { for (int32_t ny = 0; ny < height; ny++) { Tile *tile = g_game.map.getTile(x + nx + offset, y + ny + offset, z); if (tile) { if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } skip = 0; GetTileDescription(tile, msg); } else if (skip == 0xFE) { msg.addByte(0xFF); msg.addByte(0xFF); skip = -1; } else { ++skip; } } } } void ProtocolGame::checkCreatureAsKnown(uint32_t id, bool &known, uint32_t &removedKnown) { auto result = knownCreatureSet.insert(id); if (!result.second) { known = true; return; } known = false; if (knownCreatureSet.size() > 1300) { // Look for a creature to remove for (auto it = knownCreatureSet.begin(), end = knownCreatureSet.end(); it != end; ++it) { Creature *creature = g_game.getCreatureByID(*it); if (!canSee(creature)) { removedKnown = *it; knownCreatureSet.erase(it); return; } } // Bad situation. Let's just remove anyone. auto it = knownCreatureSet.begin(); if (*it == id) { ++it; } removedKnown = *it; knownCreatureSet.erase(it); } else { removedKnown = 0; } } bool ProtocolGame::canSee(const Creature *c) const { if (!c || !player || c->isRemoved()) { return false; } if (!player->canSeeCreature(c)) { return false; } return canSee(c->getPosition()); } bool ProtocolGame::canSee(const Position &pos) const { return canSee(pos.x, pos.y, pos.z); } bool ProtocolGame::canSee(int32_t x, int32_t y, int32_t z) const { if (!player) { return false; } const Position &myPos = player->getPosition(); if (myPos.z <= 7) { // we are on ground level or above (7 -> 0) // FIX: surface players see ALL floors 0-15 (no restriction). // z is already capped at MAP_MAX_LAYERS-1 by position validation, // so no additional check needed here. } else { // we are underground (8 -> 15) // view is +/- 5 from the floor we stand on if (std::abs(myPos.getZ() - z) > 5) { return false; } } // negative offset means that the action taken place is on a lower floor than // ourself int32_t offsetz = myPos.getZ() - z; if ((x >= myPos.getX() - awareRange.left() + offsetz) && (x <= myPos.getX() + awareRange.right() + offsetz) && (y >= myPos.getY() - awareRange.top() + offsetz) && (y <= myPos.getY() + awareRange.bottom() + offsetz)) { return true; } return false; } // Parse methods void ProtocolGame::parseChannelInvite(NetworkMessage &msg) { const std::string name = msg.getString(); addGameTask(&Game::playerChannelInvite, player->getID(), name); } void ProtocolGame::parseChannelExclude(NetworkMessage &msg) { const std::string name = msg.getString(); addGameTask(&Game::playerChannelExclude, player->getID(), name); } void ProtocolGame::parseOpenChannel(NetworkMessage &msg) { uint16_t channelId = msg.get(); addGameTask(&Game::playerOpenChannel, player->getID(), channelId); } void ProtocolGame::parseCloseChannel(NetworkMessage &msg) { uint16_t channelId = msg.get(); addGameTask(&Game::playerCloseChannel, player->getID(), channelId); } void ProtocolGame::parseOpenPrivateChannel(NetworkMessage &msg) { const std::string receiver = msg.getString(); addGameTask(&Game::playerOpenPrivateChannel, player->getID(), receiver); } void ProtocolGame::parseAutoWalk(NetworkMessage &msg) { uint8_t numdirs = msg.getByte(); if (numdirs == 0 || (msg.getBufferPosition() + numdirs) != (msg.getLength() + 8)) { return; } msg.skipBytes(numdirs); std::list path; for (uint8_t i = 0; i < numdirs; ++i) { uint8_t rawdir = msg.getPreviousByte(); switch (rawdir) { case 1: path.push_front(DIRECTION_EAST); break; case 2: path.push_front(DIRECTION_NORTHEAST); break; case 3: path.push_front(DIRECTION_NORTH); break; case 4: path.push_front(DIRECTION_NORTHWEST); break; case 5: path.push_front(DIRECTION_WEST); break; case 6: path.push_front(DIRECTION_SOUTHWEST); break; case 7: path.push_front(DIRECTION_SOUTH); break; case 8: path.push_front(DIRECTION_SOUTHEAST); break; default: break; } } if (path.empty()) { return; } addGameTask(&Game::playerAutoWalk, player->getID(), path); } void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { Outfit_t newOutfit; newOutfit.lookType = msg.get(); newOutfit.lookHead = msg.getByte(); newOutfit.lookBody = msg.getByte(); newOutfit.lookLegs = msg.getByte(); newOutfit.lookFeet = msg.getByte(); newOutfit.lookAddons = msg.getByte(); newOutfit.lookMount = msg.get(); newOutfit.lookWings = otclientV8 ? msg.get() : 0; newOutfit.lookAura = otclientV8 ? msg.get() : 0; std::string shaderName = otclientV8 ? msg.getString() : ""; Shader *shader = g_game.shaders.getShaderByName(shaderName); newOutfit.lookShader = shader ? shader->id : 0; newOutfit.lookRuneEffect = otclientV8 ? msg.get() : 0; addGameTask(&Game::playerChangeOutfit, player->getID(), newOutfit); } void ProtocolGame::parseToggleMount(NetworkMessage &msg) { int mount = msg.get(); int wings = -1, aura = -1, shader = -1, runeeffect = -1; if (otclientV8 >= 254) { wings = msg.get(); aura = msg.get(); shader = msg.get(); runeeffect = msg.get(); } addGameTask(&Game::playerToggleOutfitExtension, player->getID(), mount, wings, aura, shader, runeeffect); } void ProtocolGame::parseUseItem(NetworkMessage &msg) { Position pos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t stackpos = msg.getByte(); uint8_t index = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseItem, player->getID(), pos, stackpos, index, spriteId); } void ProtocolGame::parseUseItemEx(NetworkMessage &msg) { Position fromPos = msg.getPosition(); uint16_t fromSpriteId = msg.get(); uint8_t fromStackPos = msg.getByte(); Position toPos = msg.getPosition(); uint16_t toSpriteId = msg.get(); uint8_t toStackPos = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseItemEx, player->getID(), fromPos, fromStackPos, fromSpriteId, toPos, toStackPos, toSpriteId); } void ProtocolGame::parseUseWithCreature(NetworkMessage &msg) { Position fromPos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t fromStackPos = msg.getByte(); uint32_t creatureId = msg.get(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseWithCreature, player->getID(), fromPos, fromStackPos, creatureId, spriteId); } void ProtocolGame::parseCloseContainer(NetworkMessage &msg) { uint8_t cid = msg.getByte(); addGameTask(&Game::playerCloseContainer, player->getID(), cid); } void ProtocolGame::parseUpArrowContainer(NetworkMessage &msg) { uint8_t cid = msg.getByte(); addGameTask(&Game::playerMoveUpContainer, player->getID(), cid); } void ProtocolGame::parseUpdateContainer(NetworkMessage &msg) { uint8_t cid = msg.getByte(); addGameTask(&Game::playerUpdateContainer, player->getID(), cid); } void ProtocolGame::parseThrow(NetworkMessage &msg) { Position fromPos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t fromStackpos = msg.getByte(); Position toPos = msg.getPosition(); uint8_t count = msg.getByte(); if (toPos != fromPos) { addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerMoveThing, player->getID(), fromPos, spriteId, fromStackpos, toPos, count); } } void ProtocolGame::parseLookAt(NetworkMessage &msg) { Position pos = msg.getPosition(); msg.skipBytes(2); // spriteId uint8_t stackpos = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookAt, player->getID(), pos, stackpos); } void ProtocolGame::parseLookInBattleList(NetworkMessage &msg) { uint32_t creatureId = msg.get(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInBattleList, player->getID(), creatureId); } void ProtocolGame::parseSay(NetworkMessage &msg) { std::string receiver; uint16_t channelId; SpeakClasses type = static_cast(msg.getByte()); switch (type) { case TALKTYPE_PRIVATE_TO: case TALKTYPE_PRIVATE_RED_TO: receiver = msg.getString(); channelId = 0; break; case TALKTYPE_CHANNEL_Y: case TALKTYPE_CHANNEL_R1: channelId = msg.get(); break; default: channelId = 0; break; } const std::string text = msg.getString(); if (text.length() > 255) { return; } addGameTask(&Game::playerSay, player->getID(), channelId, type, receiver, text); } void ProtocolGame::parseFightModes(NetworkMessage &msg) { uint8_t rawFightMode = msg.getByte(); // 1 - offensive, 2 - balanced, 3 - defensive uint8_t rawChaseMode = msg.getByte(); // 0 - stand while fightning, 1 - chase opponent uint8_t rawSecureMode = msg.getByte(); // 0 - can't attack unmarked, 1 - can attack unmarked // uint8_t rawPvpMode = msg.getByte(); // pvp mode introduced in 10.0 fightMode_t fightMode; if (rawFightMode == 1) { fightMode = FIGHTMODE_ATTACK; } else if (rawFightMode == 2) { fightMode = FIGHTMODE_BALANCED; } else { fightMode = FIGHTMODE_DEFENSE; } addGameTask(&Game::playerSetFightModes, player->getID(), fightMode, rawChaseMode != 0, rawSecureMode != 0); } void ProtocolGame::parseAttack(NetworkMessage &msg) { uint32_t creatureId = msg.get(); // msg.get(); creatureId (same as above) addGameTask(&Game::playerSetAttackedCreature, player->getID(), creatureId); } void ProtocolGame::parseFollow(NetworkMessage &msg) { uint32_t creatureId = msg.get(); // msg.get(); creatureId (same as above) addGameTask(&Game::playerFollowCreature, player->getID(), creatureId); } void ProtocolGame::parseEquipObject(NetworkMessage &msg) { uint16_t spriteId = msg.get(); // msg.get(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerEquipItem, player->getID(), spriteId); } void ProtocolGame::parseTextWindow(NetworkMessage &msg) { uint32_t windowTextId = msg.get(); const std::string newText = msg.getString(); addGameTask(&Game::playerWriteItem, player->getID(), windowTextId, newText); } void ProtocolGame::parseHouseWindow(NetworkMessage &msg) { uint8_t doorId = msg.getByte(); uint32_t id = msg.get(); const std::string text = msg.getString(); addGameTask(&Game::playerUpdateHouseWindow, player->getID(), doorId, id, text); } void ProtocolGame::parseWrapItem(NetworkMessage &msg) { Position pos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t stackpos = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerWrapItem, player->getID(), pos, stackpos, spriteId); } void ProtocolGame::parseLookInShop(NetworkMessage &msg) { uint16_t id = msg.get(); uint8_t count = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInShop, player->getID(), id, count); } void ProtocolGame::parsePlayerPurchase(NetworkMessage &msg) { uint16_t id = msg.get(); uint8_t count = msg.getByte(); uint8_t amount = msg.getByte(); bool ignoreCap = msg.getByte() != 0; bool inBackpacks = msg.getByte() != 0; addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerPurchaseItem, player->getID(), id, count, amount, ignoreCap, inBackpacks); } void ProtocolGame::parsePlayerSale(NetworkMessage &msg) { uint16_t id = msg.get(); uint8_t count = msg.getByte(); uint8_t amount = msg.getByte(); bool ignoreEquipped = msg.getByte() != 0; addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerSellItem, player->getID(), id, count, amount, ignoreEquipped); } void ProtocolGame::parseRequestTrade(NetworkMessage &msg) { Position pos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t stackpos = msg.getByte(); uint32_t playerId = msg.get(); addGameTask(&Game::playerRequestTrade, player->getID(), pos, stackpos, playerId, spriteId); } void ProtocolGame::parseLookInTrade(NetworkMessage &msg) { bool counterOffer = (msg.getByte() == 0x01); uint8_t index = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInTrade, player->getID(), counterOffer, index); } void ProtocolGame::parseAddVip(NetworkMessage &msg) { const std::string name = msg.getString(); addGameTask(&Game::playerRequestAddVip, player->getID(), name); } void ProtocolGame::parseRemoveVip(NetworkMessage &msg) { uint32_t guid = msg.get(); addGameTask(&Game::playerRequestRemoveVip, player->getID(), guid); } void ProtocolGame::parseEditVip(NetworkMessage &msg) { uint32_t guid = msg.get(); const std::string description = msg.getString(); uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63 bool notify = msg.getByte() != 0; addGameTask(&Game::playerRequestEditVip, player->getID(), guid, description, icon, notify); } void ProtocolGame::parseRotateItem(NetworkMessage &msg) { Position pos = msg.getPosition(); uint16_t spriteId = msg.get(); uint8_t stackpos = msg.getByte(); addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerRotateItem, player->getID(), pos, stackpos, spriteId); } void ProtocolGame::parseRuleViolationReport(NetworkMessage &msg) { uint8_t reportType = msg.getByte(); uint8_t reportReason = msg.getByte(); const std::string &targetName = msg.getString(); const std::string &comment = msg.getString(); std::string translation; if (reportType == REPORT_TYPE_NAME) { translation = msg.getString(); } else if (reportType == REPORT_TYPE_STATEMENT) { translation = msg.getString(); msg.get(); // statement id, used to get whatever player have said, // we don't log that. } addGameTask(&Game::playerReportRuleViolation, player->getID(), targetName, reportType, reportReason, comment, translation); } void ProtocolGame::parseBugReport(NetworkMessage &msg) { uint8_t category = msg.getByte(); std::string message = msg.getString(); Position position; if (category == BUG_CATEGORY_MAP) { position = msg.getPosition(); } addGameTask(&Game::playerReportBug, player->getID(), message, position, category); } void ProtocolGame::parseDebugAssert(NetworkMessage &msg) { if (debugAssertSent) { return; } debugAssertSent = true; std::string assertLine = msg.getString(); std::string date = msg.getString(); std::string description = msg.getString(); std::string comment = msg.getString(); addGameTask(&Game::playerDebugAssert, player->getID(), assertLine, date, description, comment); } void ProtocolGame::parseInviteToParty(NetworkMessage &msg) { uint32_t targetId = msg.get(); addGameTask(&Game::playerInviteToParty, player->getID(), targetId); } void ProtocolGame::parseJoinParty(NetworkMessage &msg) { uint32_t targetId = msg.get(); addGameTask(&Game::playerJoinParty, player->getID(), targetId); } void ProtocolGame::parseRevokePartyInvite(NetworkMessage &msg) { uint32_t targetId = msg.get(); addGameTask(&Game::playerRevokePartyInvitation, player->getID(), targetId); } void ProtocolGame::parsePassPartyLeadership(NetworkMessage &msg) { uint32_t targetId = msg.get(); addGameTask(&Game::playerPassPartyLeadership, player->getID(), targetId); } void ProtocolGame::parseEnableSharedPartyExperience(NetworkMessage &msg) { bool sharedExpActive = msg.getByte() == 1; addGameTask(&Game::playerEnableSharedPartyExperience, player->getID(), sharedExpActive); } void ProtocolGame::parseQuestLine(NetworkMessage &msg) { uint16_t questId = msg.get(); addGameTask(&Game::playerShowQuestLine, player->getID(), questId); } void ProtocolGame::parseMarketLeave() { addGameTask(&Game::playerLeaveMarket, player->getID()); } void ProtocolGame::parseMarketBrowse(NetworkMessage &msg) { uint16_t browseId = msg.get(); if (browseId == MARKETREQUEST_OWN_OFFERS) { addGameTask(&Game::playerBrowseMarketOwnOffers, player->getID()); } else if (browseId == MARKETREQUEST_OWN_HISTORY) { addGameTask(&Game::playerBrowseMarketOwnHistory, player->getID()); } else { addGameTask(&Game::playerBrowseMarket, player->getID(), browseId); } } void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) { uint8_t type = msg.getByte(); uint16_t spriteId = msg.get(); uint16_t amount = msg.get(); uint32_t price = msg.get(); bool anonymous = (msg.getByte() != 0); addGameTask(&Game::playerCreateMarketOffer, player->getID(), type, spriteId, amount, price, anonymous); } void ProtocolGame::parseMarketCancelOffer(NetworkMessage &msg) { uint32_t timestamp = msg.get(); uint16_t counter = msg.get(); addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter); } void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) { uint32_t timestamp = msg.get(); uint16_t counter = msg.get(); uint16_t amount = msg.get(); addGameTask(&Game::playerAcceptMarketOffer, player->getID(), timestamp, counter, amount); } void ProtocolGame::parseModalWindowAnswer(NetworkMessage &msg) { uint32_t id = msg.get(); uint8_t button = msg.getByte(); uint8_t choice = msg.getByte(); addGameTask(&Game::playerAnswerModalWindow, player->getID(), id, button, choice); } void ProtocolGame::parseBrowseField(NetworkMessage &msg) { const Position &pos = msg.getPosition(); addGameTask(&Game::playerBrowseField, player->getID(), pos); } void ProtocolGame::parseSeekInContainer(NetworkMessage &msg) { uint8_t containerId = msg.getByte(); uint16_t index = msg.get(); addGameTask(&Game::playerSeekInContainer, player->getID(), containerId, index); } // Send methods void ProtocolGame::sendOpenPrivateChannel(const std::string &receiver) { NetworkMessage msg; msg.addByte(0xAD); msg.addString(receiver); writeToOutputBuffer(msg); } void ProtocolGame::sendChannelEvent(uint16_t channelId, const std::string &playerName, ChannelEvent_t channelEvent) { NetworkMessage msg; msg.addByte(0xF3); msg.add(channelId); msg.addString(playerName); msg.addByte(channelEvent); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureOutfit(const Creature *creature, const Outfit_t &outfit) { if (!canSee(creature)) { return; } Outfit_t finalOutfit = outfit; const Player *player = creature->getPlayer(); if (player) { const Outfit *outfitDef = Outfits::getInstance().getOutfitByLookType(player->getSex(), finalOutfit.lookType); if (outfitDef) { finalOutfit.attackRange = outfitDef->attackRange; for (int d = 0; d < OUTFIT_DIRECTIONS; ++d) { finalOutfit.offsets[d] = outfitDef->offsets[d]; } } } NetworkMessage msg; msg.addByte(0x8E); msg.add(creature->getID()); AddOutfit(msg, finalOutfit, creature->getAttackOutfitLookType()); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureAttackPulse(const Creature *creature, const Outfit_t &outfit, uint32_t duration) { if (!canSee(creature)) { return; } Outfit_t finalOutfit = outfit; const Player *player = creature->getPlayer(); if (player) { const Outfit *outfitDef = Outfits::getInstance().getOutfitByLookType(player->getSex(), creature->getCurrentOutfit().lookType); if (outfitDef) { finalOutfit.attackRange = outfitDef->attackRange; for (int d = 0; d < OUTFIT_DIRECTIONS; ++d) { finalOutfit.offsets[d] = outfitDef->offsets[d]; } } } NetworkMessage msg; msg.addByte(0x5E); msg.add(creature->getID()); AddOutfit(msg, finalOutfit, creature->getAttackOutfitLookType()); msg.add(duration); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureLight(const Creature *creature) { if (!canSee(creature)) { return; } NetworkMessage msg; AddCreatureLight(msg, creature); writeToOutputBuffer(msg); } void ProtocolGame::sendWorldLight(LightInfo lightInfo) { NetworkMessage msg; AddWorldLight(msg, lightInfo); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureWalkthrough(const Creature *creature, bool walkthrough) { if (!canSee(creature)) { return; } NetworkMessage msg; msg.addByte(0x92); msg.add(creature->getID()); msg.addByte(walkthrough ? 0x00 : 0x01); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureShield(const Creature *creature) { if (!canSee(creature)) { return; } NetworkMessage msg; msg.addByte(0x91); msg.add(creature->getID()); msg.addByte(player->getPartyShield(creature->getPlayer())); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureSkull(const Creature *creature) { if (g_game.getWorldType() != WORLD_TYPE_PVP) { return; } if (!canSee(creature)) { return; } NetworkMessage msg; msg.addByte(0x90); msg.add(creature->getID()); msg.addByte(player->getSkullClient(creature)); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureType(uint32_t creatureId, uint8_t creatureType) { NetworkMessage msg; msg.addByte(0x95); msg.add(creatureId); msg.addByte(creatureType); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureHelpers(uint32_t creatureId, uint16_t helpers) { NetworkMessage msg; msg.addByte(0x94); msg.add(creatureId); msg.add(helpers); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureSquare(const Creature *creature, SquareColor_t color) { if (!canSee(creature)) { return; } NetworkMessage msg; msg.addByte(0x93); msg.add(creature->getID()); msg.addByte(0x01); msg.addByte(color); writeToOutputBuffer(msg); } void ProtocolGame::sendTutorial(uint8_t tutorialId) { NetworkMessage msg; msg.addByte(0xDC); msg.addByte(tutorialId); writeToOutputBuffer(msg); } void ProtocolGame::sendAddMarker(const Position &pos, uint8_t markType, const std::string &desc) { NetworkMessage msg; msg.addByte(0xDD); msg.addPosition(pos); msg.addByte(markType); msg.addString(desc); writeToOutputBuffer(msg); } void ProtocolGame::sendReLoginWindow(uint8_t unfairFightReduction) { NetworkMessage msg; msg.addByte(0x28); msg.addByte(0x00); msg.addByte(unfairFightReduction); writeToOutputBuffer(msg); } void ProtocolGame::sendStats() { NetworkMessage msg; AddPlayerStats(msg); writeToOutputBuffer(msg); } void ProtocolGame::sendBasicData() { NetworkMessage msg; msg.addByte(0x9F); if (player->isPremium()) { msg.addByte(1); msg.add(time(nullptr) + (player->premiumDays * 86400)); } else { msg.addByte(0); msg.add(0); } msg.addByte(player->getVocation()->getClientId()); msg.add(0xFF); // number of known spells for (uint8_t spellId = 0x00; spellId < 0xFF; spellId++) { msg.addByte(spellId); } writeToOutputBuffer(msg); } void ProtocolGame::sendTextMessage(const TextMessage &message) { NetworkMessage msg; msg.addByte(0xB4); msg.addByte(message.type); switch (message.type) { case MESSAGE_DAMAGE_DEALT: case MESSAGE_DAMAGE_RECEIVED: case MESSAGE_DAMAGE_OTHERS: { msg.addPosition(message.position); msg.add(message.primary.value); msg.addByte(message.primary.color); msg.add(message.secondary.value); msg.addByte(message.secondary.color); break; } case MESSAGE_HEALED: case MESSAGE_HEALED_OTHERS: case MESSAGE_EXPERIENCE: case MESSAGE_EXPERIENCE_OTHERS: { msg.addPosition(message.position); msg.add(message.primary.value); msg.addByte(message.primary.color); break; } case MESSAGE_GUILD: case MESSAGE_PARTY_MANAGEMENT: case MESSAGE_PARTY: msg.add(message.channelId); break; default: { break; } } msg.addString(message.text); writeToOutputBuffer(msg); } void ProtocolGame::sendClosePrivate(uint16_t channelId) { NetworkMessage msg; msg.addByte(0xB3); msg.add(channelId); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatePrivateChannel(uint16_t channelId, const std::string &channelName) { NetworkMessage msg; msg.addByte(0xB2); msg.add(channelId); msg.addString(channelName); msg.add(0x01); msg.addString(player->getName()); msg.add(0x00); writeToOutputBuffer(msg); } void ProtocolGame::sendChannelsDialog() { NetworkMessage msg; msg.addByte(0xAB); const ChannelList &list = g_chat->getChannelList(*player); msg.addByte(list.size()); for (ChatChannel *channel : list) { msg.add(channel->getId()); msg.addString(channel->getName()); } writeToOutputBuffer(msg); } void ProtocolGame::sendChannel(uint16_t channelId, const std::string &channelName, const UsersMap *channelUsers, const InvitedMap *invitedUsers) { NetworkMessage msg; msg.addByte(0xAC); msg.add(channelId); msg.addString(channelName); if (channelUsers) { msg.add(channelUsers->size()); for (const auto &it : *channelUsers) { msg.addString(it.second->getName()); } } else { msg.add(0x00); } if (invitedUsers) { msg.add(invitedUsers->size()); for (const auto &it : *invitedUsers) { msg.addString(it.second->getName()); } } else { msg.add(0x00); } writeToOutputBuffer(msg); } void ProtocolGame::sendChannelMessage(const std::string &author, const std::string &text, SpeakClasses type, uint16_t channel) { NetworkMessage msg; msg.addByte(0xAA); msg.add(0x00); msg.addString(author); msg.add(0x00); msg.addByte(type); msg.add(channel); msg.addString(text); writeToOutputBuffer(msg); } void ProtocolGame::sendIcons(uint16_t icons) { NetworkMessage msg; msg.addByte(0xA2); msg.add(icons); writeToOutputBuffer(msg); } void ProtocolGame::sendContainer(uint8_t cid, const Container *container, bool hasParent, uint16_t firstIndex) { NetworkMessage msg; msg.addByte(0x6E); msg.addByte(cid); if (container->getID() == ITEM_BROWSEFIELD) { msg.addItem(ITEM_BAG, 1, otclientV8); msg.addString("Browse Field"); } else { msg.addItem(container, otclientV8); msg.addString(container->getName()); } msg.addByte(container->capacity()); msg.addByte(hasParent ? 0x01 : 0x00); msg.addByte(container->isUnlocked() ? 0x01 : 0x00); // Drag and drop msg.addByte(container->hasPagination() ? 0x01 : 0x00); // Pagination uint32_t containerSize = container->size(); msg.add(containerSize); msg.add(firstIndex); if (firstIndex < containerSize) { uint8_t itemsToSend = std::min( std::min(container->capacity(), containerSize - firstIndex), std::numeric_limits::max()); msg.addByte(itemsToSend); for (auto it = container->getItemList().begin() + firstIndex, end = it + itemsToSend; it != end; ++it) { msg.addItem(*it, otclientV8); } } else { msg.addByte(0x00); } writeToOutputBuffer(msg); } void ProtocolGame::sendShop(Npc *npc, const ShopInfoList &itemList) { NetworkMessage msg; msg.addByte(0x7A); msg.addString(npc->getName()); uint16_t itemsToSend = std::min(itemList.size(), std::numeric_limits::max()); msg.add(itemsToSend); uint16_t i = 0; for (auto it = itemList.begin(); i < itemsToSend; ++it, ++i) { AddShopItem(msg, *it); } writeToOutputBuffer(msg); } void ProtocolGame::sendCloseShop() { NetworkMessage msg; msg.addByte(0x7C); writeToOutputBuffer(msg); } void ProtocolGame::sendSaleItemList(const std::list &shop) { NetworkMessage msg; msg.addByte(0x7B); msg.add(player->getMoney() + player->getBankBalance()); std::map saleMap; if (shop.size() <= 5) { // For very small shops it's not worth it to create the complete map for (const ShopInfo &shopInfo : shop) { if (shopInfo.sellPrice == 0) { continue; } int8_t subtype = -1; const ItemType &itemType = Item::items[shopInfo.itemId]; if (itemType.hasSubType() && !itemType.stackable) { subtype = (shopInfo.subType == 0 ? -1 : shopInfo.subType); } uint32_t count = player->getItemTypeCount(shopInfo.itemId, subtype); if (count > 0) { saleMap[shopInfo.itemId] = count; } } } else { // Large shop, it's better to get a cached map of all item counts and use it // We need a temporary map since the finished map should only contain items // available in the shop std::map tempSaleMap; player->getAllItemTypeCount(tempSaleMap); // We must still check manually for the special items that require subtype // matches (That is, fluids such as potions etc., actually these items are // very few since health potions now use their own ID) for (const ShopInfo &shopInfo : shop) { if (shopInfo.sellPrice == 0) { continue; } int8_t subtype = -1; const ItemType &itemType = Item::items[shopInfo.itemId]; if (itemType.hasSubType() && !itemType.stackable) { subtype = (shopInfo.subType == 0 ? -1 : shopInfo.subType); } if (subtype != -1) { uint32_t count; if (!itemType.isFluidContainer() && !itemType.isSplash()) { count = player->getItemTypeCount( shopInfo.itemId, subtype); // This shop item requires extra checks } else { count = subtype; } if (count > 0) { saleMap[shopInfo.itemId] = count; } } else { std::map::const_iterator findIt = tempSaleMap.find(shopInfo.itemId); if (findIt != tempSaleMap.end() && findIt->second > 0) { saleMap[shopInfo.itemId] = findIt->second; } } } } uint8_t itemsToSend = std::min(saleMap.size(), std::numeric_limits::max()); msg.addByte(itemsToSend); uint8_t i = 0; for (std::map::const_iterator it = saleMap.begin(); i < itemsToSend; ++it, ++i) { msg.addItemId(it->first); msg.addByte( std::min(it->second, std::numeric_limits::max())); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketEnter(uint32_t depotId) { NetworkMessage msg; msg.addByte(0xF6); msg.add(player->getBankBalance()); msg.addByte( std::min(IOMarket::getPlayerOfferCount(player->getGUID()), std::numeric_limits::max())); DepotChest *depotChest = player->getDepotChest(depotId, false); if (!depotChest) { msg.add(0x00); writeToOutputBuffer(msg); return; } player->setInMarket(true); std::map depotItems; std::forward_list containerList{depotChest, player->getInbox()}; do { Container *container = containerList.front(); containerList.pop_front(); for (Item *item : container->getItemList()) { Container *c = item->getContainer(); if (c && !c->empty()) { containerList.push_front(c); continue; } const ItemType &itemType = Item::items[item->getID()]; if (itemType.wareId == 0) { continue; } if (c && (!itemType.isContainer() || c->capacity() != itemType.maxItems)) { continue; } if (!item->hasMarketAttributes()) { continue; } depotItems[itemType.wareId] += Item::countByType(item, -1); } } while (!containerList.empty()); uint16_t itemsToSend = std::min(depotItems.size(), std::numeric_limits::max()); msg.add(itemsToSend); uint16_t i = 0; for (std::map::const_iterator it = depotItems.begin(); i < itemsToSend; ++it, ++i) { msg.add(it->first); msg.add(std::min(0xFFFF, it->second)); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketLeave() { NetworkMessage msg; msg.addByte(0xF7); writeToOutputBuffer(msg); } void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList &buyOffers, const MarketOfferList &sellOffers) { NetworkMessage msg; msg.addByte(0xF9); msg.addItemId(itemId); msg.add(buyOffers.size()); for (const MarketOffer &offer : buyOffers) { msg.add(offer.timestamp); msg.add(offer.counter); msg.add(offer.amount); msg.add(offer.price); msg.addString(offer.playerName); } msg.add(sellOffers.size()); for (const MarketOffer &offer : sellOffers) { msg.add(offer.timestamp); msg.add(offer.counter); msg.add(offer.amount); msg.add(offer.price); msg.addString(offer.playerName); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx &offer) { NetworkMessage msg; msg.addByte(0xF9); msg.addItemId(offer.itemId); if (offer.type == MARKETACTION_BUY) { msg.add(0x01); msg.add(offer.timestamp); msg.add(offer.counter); msg.add(offer.amount); msg.add(offer.price); msg.addString(offer.playerName); msg.add(0x00); } else { msg.add(0x00); msg.add(0x01); msg.add(offer.timestamp); msg.add(offer.counter); msg.add(offer.amount); msg.add(offer.price); msg.addString(offer.playerName); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketBrowseOwnOffers( const MarketOfferList &buyOffers, const MarketOfferList &sellOffers) { NetworkMessage msg; msg.addByte(0xF9); msg.add(MARKETREQUEST_OWN_OFFERS); msg.add(buyOffers.size()); for (const MarketOffer &offer : buyOffers) { msg.add(offer.timestamp); msg.add(offer.counter); msg.addItemId(offer.itemId); msg.add(offer.amount); msg.add(offer.price); } msg.add(sellOffers.size()); for (const MarketOffer &offer : sellOffers) { msg.add(offer.timestamp); msg.add(offer.counter); msg.addItemId(offer.itemId); msg.add(offer.amount); msg.add(offer.price); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketCancelOffer(const MarketOfferEx &offer) { NetworkMessage msg; msg.addByte(0xF9); msg.add(MARKETREQUEST_OWN_OFFERS); if (offer.type == MARKETACTION_BUY) { msg.add(0x01); msg.add(offer.timestamp); msg.add(offer.counter); msg.addItemId(offer.itemId); msg.add(offer.amount); msg.add(offer.price); msg.add(0x00); } else { msg.add(0x00); msg.add(0x01); msg.add(offer.timestamp); msg.add(offer.counter); msg.addItemId(offer.itemId); msg.add(offer.amount); msg.add(offer.price); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketBrowseOwnHistory( const HistoryMarketOfferList &buyOffers, const HistoryMarketOfferList &sellOffers) { uint32_t i = 0; std::map counterMap; uint32_t buyOffersToSend = std::min( buyOffers.size(), 810 + std::max(0, 810 - sellOffers.size())); uint32_t sellOffersToSend = std::min( sellOffers.size(), 810 + std::max(0, 810 - buyOffers.size())); NetworkMessage msg; msg.addByte(0xF9); msg.add(MARKETREQUEST_OWN_HISTORY); msg.add(buyOffersToSend); for (auto it = buyOffers.begin(); i < buyOffersToSend; ++it, ++i) { msg.add(it->timestamp); msg.add(counterMap[it->timestamp]++); msg.addItemId(it->itemId); msg.add(it->amount); msg.add(it->price); msg.addByte(it->state); } counterMap.clear(); i = 0; msg.add(sellOffersToSend); for (auto it = sellOffers.begin(); i < sellOffersToSend; ++it, ++i) { msg.add(it->timestamp); msg.add(counterMap[it->timestamp]++); msg.addItemId(it->itemId); msg.add(it->amount); msg.add(it->price); msg.addByte(it->state); } writeToOutputBuffer(msg); } void ProtocolGame::sendMarketDetail(uint16_t itemId) { NetworkMessage msg; msg.addByte(0xF8); msg.addItemId(itemId); const ItemType &it = Item::items[itemId]; if (it.armor != 0) { msg.addString(std::to_string(it.armor)); } else { msg.add(0x00); } if (it.attack != 0) { // TODO: chance to hit, range // example: // "attack +x, chance to hit +y%, z fields" if (it.abilities && it.abilities->elementType != COMBAT_NONE && it.abilities->elementDamage != 0) { std::ostringstream ss; ss << it.attack << " physical +" << it.abilities->elementDamage << ' ' << getCombatName(it.abilities->elementType); msg.addString(ss.str()); } else { msg.addString(std::to_string(it.attack)); } } else { msg.add(0x00); } if (it.isContainer()) { msg.addString(std::to_string(it.maxItems)); } else { msg.add(0x00); } if (it.defense != 0) { if (it.extraDefense != 0) { std::ostringstream ss; ss << it.defense << ' ' << std::showpos << it.extraDefense << std::noshowpos; msg.addString(ss.str()); } else { msg.addString(std::to_string(it.defense)); } } else { msg.add(0x00); } if (!it.description.empty()) { const std::string &descr = it.description; if (descr.back() == '.') { msg.addString(std::string(descr, 0, descr.length() - 1)); } else { msg.addString(descr); } } else { msg.add(0x00); } if (it.decayTime != 0) { std::ostringstream ss; ss << it.decayTime << " seconds"; msg.addString(ss.str()); } else { msg.add(0x00); } if (it.abilities) { std::ostringstream ss; bool separator = false; for (size_t i = 0; i < COMBAT_COUNT; ++i) { if (it.abilities->absorbPercent[i] == 0) { continue; } if (separator) { ss << ", "; } else { separator = true; } ss << getCombatName(indexToCombatType(i)) << ' ' << std::showpos << it.abilities->absorbPercent[i] << std::noshowpos << '%'; } msg.addString(ss.str()); } else { msg.add(0x00); } if (it.minReqLevel != 0) { msg.addString(std::to_string(it.minReqLevel)); } else { msg.add(0x00); } if (it.minReqMagicLevel != 0) { msg.addString(std::to_string(it.minReqMagicLevel)); } else { msg.add(0x00); } msg.addString(it.vocationString); msg.addString(it.runeSpellName); if (it.abilities) { std::ostringstream ss; bool separator = false; for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; i++) { if (!it.abilities->skills[i]) { continue; } if (separator) { ss << ", "; } else { separator = true; } ss << getSkillName(i) << ' ' << std::showpos << it.abilities->skills[i] << std::noshowpos; } if (it.abilities->stats[STAT_MAGICPOINTS] != 0) { if (separator) { ss << ", "; } else { separator = true; } ss << "magic level " << std::showpos << it.abilities->stats[STAT_MAGICPOINTS] << std::noshowpos; } if (it.abilities->speed != 0) { if (separator) { ss << ", "; } ss << "speed " << std::showpos << (it.abilities->speed >> 1) << std::noshowpos; } msg.addString(ss.str()); } else { msg.add(0x00); } if (it.charges != 0) { msg.addString(std::to_string(it.charges)); } else { msg.add(0x00); } std::string weaponName = getWeaponName(it.weaponType); if (it.slotPosition & SLOTP_TWO_HAND) { if (!weaponName.empty()) { weaponName += ", two-handed"; } else { weaponName = "two-handed"; } } msg.addString(weaponName); if (it.weight != 0) { std::ostringstream ss; if (it.weight < 10) { ss << "0.0" << it.weight; } else if (it.weight < 100) { ss << "0." << it.weight; } else { std::string weightString = std::to_string(it.weight); weightString.insert(weightString.end() - 2, '.'); ss << weightString; } ss << " oz"; msg.addString(ss.str()); } else { msg.add(0x00); } MarketStatistics *statistics = IOMarket::getInstance().getPurchaseStatistics(itemId); if (statistics) { msg.addByte(0x01); msg.add(statistics->numTransactions); msg.add(std::min(std::numeric_limits::max(), statistics->totalPrice)); msg.add(statistics->highestPrice); msg.add(statistics->lowestPrice); } else { msg.addByte(0x00); } statistics = IOMarket::getInstance().getSaleStatistics(itemId); if (statistics) { msg.addByte(0x01); msg.add(statistics->numTransactions); msg.add(std::min(std::numeric_limits::max(), statistics->totalPrice)); msg.add(statistics->highestPrice); msg.add(statistics->lowestPrice); } else { msg.addByte(0x00); } writeToOutputBuffer(msg); } void ProtocolGame::sendQuestLog() { NetworkMessage msg; msg.addByte(0xF0); msg.add(g_game.quests.getQuestsCount(player)); for (const Quest &quest : g_game.quests.getQuests()) { if (quest.isStarted(player)) { msg.add(quest.getID()); msg.addString(quest.getName()); msg.addByte(quest.isCompleted(player)); } } writeToOutputBuffer(msg); } void ProtocolGame::sendQuestLine(const Quest *quest) { NetworkMessage msg; msg.addByte(0xF1); msg.add(quest->getID()); msg.addByte(quest->getMissionsCount(player)); for (const Mission &mission : quest->getMissions()) { if (mission.isStarted(player)) { msg.addString(mission.getName(player)); msg.addString(mission.getDescription(player)); } } writeToOutputBuffer(msg); } void ProtocolGame::sendTradeItemRequest(const std::string &traderName, const Item *item, bool ack) { NetworkMessage msg; if (ack) { msg.addByte(0x7D); } else { msg.addByte(0x7E); } msg.addString(traderName); if (const Container *tradeContainer = item->getContainer()) { std::list listContainer{tradeContainer}; std::list itemList{tradeContainer}; while (!listContainer.empty()) { const Container *container = listContainer.front(); listContainer.pop_front(); for (Item *containerItem : container->getItemList()) { Container *tmpContainer = containerItem->getContainer(); if (tmpContainer) { listContainer.push_back(tmpContainer); } itemList.push_back(containerItem); } } msg.addByte(itemList.size()); for (const Item *listItem : itemList) { msg.addItem(listItem, otclientV8); } } else { msg.addByte(0x01); msg.addItem(item, otclientV8); } writeToOutputBuffer(msg); } void ProtocolGame::sendCloseTrade() { NetworkMessage msg; msg.addByte(0x7F); writeToOutputBuffer(msg); } void ProtocolGame::sendCloseContainer(uint8_t cid) { NetworkMessage msg; msg.addByte(0x6F); msg.addByte(cid); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureTurn(const Creature *creature, uint32_t stackPos) { if (!canSee(creature)) { return; } NetworkMessage msg; msg.addByte(0x6B); msg.addPosition(creature->getPosition()); msg.addByte(stackPos); msg.add(0x63); msg.add(creature->getID()); msg.addByte(creature->getDirection()); msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureSay(const Creature *creature, SpeakClasses type, const std::string &text, const Position *pos /* = nullptr*/) { NetworkMessage msg; msg.addByte(0xAA); static uint32_t statementId = 0; msg.add(++statementId); msg.addString(creature->getName()); // Add level only for players if (const Player *speaker = creature->getPlayer()) { msg.add(speaker->getLevel()); } else { msg.add(0x00); } msg.addByte(type); if (pos) { msg.addPosition(*pos); } else { msg.addPosition(creature->getPosition()); } msg.addString(text); writeToOutputBuffer(msg); } void ProtocolGame::sendToChannel(const Creature *creature, SpeakClasses type, const std::string &text, uint16_t channelId) { NetworkMessage msg; msg.addByte(0xAA); static uint32_t statementId = 0; msg.add(++statementId); if (!creature) { msg.add(0x00); } else if (type == TALKTYPE_CHANNEL_R2) { msg.add(0x00); type = TALKTYPE_CHANNEL_R1; } else { msg.addString(creature->getName()); // Add level only for players if (const Player *speaker = creature->getPlayer()) { msg.add(speaker->getLevel()); } else { msg.add(0x00); } } msg.addByte(type); msg.add(channelId); msg.addString(text); writeToOutputBuffer(msg); } void ProtocolGame::sendPrivateMessage(const Player *speaker, SpeakClasses type, const std::string &text) { NetworkMessage msg; msg.addByte(0xAA); static uint32_t statementId = 0; msg.add(++statementId); if (speaker) { msg.addString(speaker->getName()); msg.add(speaker->getLevel()); } else { msg.add(0x00); } msg.addByte(type); msg.addString(text); writeToOutputBuffer(msg); } void ProtocolGame::sendCancelTarget() { NetworkMessage msg; msg.addByte(0xA3); msg.add(0x00); writeToOutputBuffer(msg); } void ProtocolGame::sendChangeSpeed(const Creature *creature, uint32_t speed) { NetworkMessage msg; msg.addByte(0x8F); msg.add(creature->getID()); msg.add(creature->getBaseSpeed() / 2); msg.add(speed / 2); writeToOutputBuffer(msg); } void ProtocolGame::sendCancelWalk() { NetworkMessage msg; msg.addByte(0xB5); msg.addByte(player->getDirection()); writeToOutputBuffer(msg); } void ProtocolGame::sendSkills() { NetworkMessage msg; AddPlayerSkills(msg); writeToOutputBuffer(msg); } void ProtocolGame::sendPing() { NetworkMessage msg; msg.addByte(0x1D); writeToOutputBuffer(msg); } void ProtocolGame::sendPingBack() { NetworkMessage msg; msg.addByte(0x1E); writeToOutputBuffer(msg); } void ProtocolGame::sendDistanceShoot(const Position &from, const Position &to, uint8_t type) { NetworkMessage msg; msg.addByte(0x85); msg.addPosition(from); msg.addPosition(to); msg.addByte(type); writeToOutputBuffer(msg); } void ProtocolGame::sendMagicEffect(const Position &pos, uint16_t type) { if (!canSee(pos)) { return; } NetworkMessage msg; msg.addByte(0x83); msg.addPosition(pos); msg.add(type); writeToOutputBuffer(msg); } void ProtocolGame::sendCreatureHealth(const Creature *creature) { NetworkMessage msg; msg.addByte(0x8C); msg.add(creature->getID()); if (creature->isHealthHidden()) { msg.addByte(0x00); } else { msg.addByte(std::ceil((static_cast(creature->getHealth()) / std::max(creature->getMaxHealth(), 1)) * 100)); } writeToOutputBuffer(msg); } void ProtocolGame::sendFYIBox(const std::string &message) { NetworkMessage msg; msg.addByte(0x15); msg.addString(message); writeToOutputBuffer(msg); } // tile void ProtocolGame::sendMapDescription(const Position &pos) { if (otclientV8) { int32_t startz, endz, zstep; if (pos.z > 7) { startz = pos.z - 5; endz = std::min(MAP_MAX_LAYERS - 1, pos.z + 5); zstep = 1; } else { // FIX: send ALL floors (15->0) for surface players. startz = MAP_MAX_LAYERS - 1; // = 15 endz = 0; zstep = -1; } for (int32_t nz = startz; nz != endz + zstep; nz += zstep) { sendFloorDescription(pos, nz); } } else { NetworkMessage msg; msg.addByte(0x64); msg.addPosition(player->getPosition()); GetMapDescription(pos.x - awareRange.left(), pos.y - awareRange.top(), pos.z, awareRange.horizontal(), awareRange.vertical(), msg); writeToOutputBuffer(msg); } } void ProtocolGame::sendFloorDescription(const Position &pos, int floor) { // When map view range is big, let's say 30x20 all floors may not fit in // single packets So we split one packet with every floor to few packets with // single floor NetworkMessage msg; msg.addByte(0x4B); msg.addPosition(player->getPosition()); msg.addByte(floor); int32_t skip = -1; GetFloorDescription(msg, pos.x - awareRange.left(), pos.y - awareRange.top(), floor, awareRange.horizontal(), awareRange.vertical(), pos.z - floor, skip); if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } writeToOutputBuffer(msg); } void ProtocolGame::sendAddTileItem(const Position &pos, uint32_t stackpos, const Item *item) { if (!canSee(pos)) { return; } NetworkMessage msg; msg.addByte(0x6A); msg.addPosition(pos); msg.addByte(stackpos); msg.addItem(item); writeToOutputBuffer(msg); checkPredictiveWalking(pos); } void ProtocolGame::sendUpdateTileItem(const Position &pos, uint32_t stackpos, const Item *item) { if (!canSee(pos)) { return; } NetworkMessage msg; msg.addByte(0x6B); msg.addPosition(pos); msg.addByte(stackpos); msg.addItem(item); writeToOutputBuffer(msg); checkPredictiveWalking(pos); } void ProtocolGame::sendRemoveTileThing(const Position &pos, uint32_t stackpos) { if (!canSee(pos)) { return; } NetworkMessage msg; RemoveTileThing(msg, pos, stackpos); writeToOutputBuffer(msg); } void ProtocolGame::sendUpdateTile(const Tile *tile, const Position &pos) { if (!canSee(pos)) { return; } NetworkMessage msg; msg.addByte(0x69); msg.addPosition(pos); if (tile) { GetTileDescription(tile, msg); msg.addByte(0x00); msg.addByte(0xFF); } else { msg.addByte(0x01); msg.addByte(0xFF); } writeToOutputBuffer(msg); } void ProtocolGame::sendPendingStateEntered() { NetworkMessage msg; msg.addByte(0x0A); writeToOutputBuffer(msg); } void ProtocolGame::sendEnterWorld() { NetworkMessage msg; msg.addByte(0x0F); writeToOutputBuffer(msg); } void ProtocolGame::sendFightModes() { NetworkMessage msg; msg.addByte(0xA7); msg.addByte(player->fightMode); msg.addByte(player->chaseMode); msg.addByte(player->secureMode); msg.addByte(PVP_MODE_DOVE); writeToOutputBuffer(msg); } void ProtocolGame::sendAddCreature(const Creature *creature, const Position &pos, int32_t stackpos, bool isLogin) { if (!canSee(pos)) { return; } if (creature != player) { if (stackpos != -1) { NetworkMessage msg; msg.addByte(0x6A); msg.addPosition(pos); msg.addByte(stackpos); bool known; uint32_t removedKnown; checkCreatureAsKnown(creature->getID(), known, removedKnown); AddCreature(msg, creature, known, removedKnown); writeToOutputBuffer(msg); } if (isLogin) { sendMagicEffect(pos, CONST_ME_TELEPORT); } checkPredictiveWalking(pos); return; } NetworkMessage msg; msg.addByte(0x17); msg.add(player->getID()); msg.add(0x32); // beat duration (50) msg.addDouble(Creature::speedA, 3); msg.addDouble(Creature::speedB, 3); msg.addDouble(Creature::speedC, 3); // can report bugs? if (player->getAccountType() >= ACCOUNT_TYPE_TUTOR) { msg.addByte(0x01); } else { msg.addByte(0x00); } msg.addByte(0x00); // can change pvp framing option msg.addByte(0x00); // expert mode button enabled msg.add(0x00); // URL (string) to ingame store images msg.add(25); // premium coin package size writeToOutputBuffer(msg); sendPendingStateEntered(); sendEnterWorld(); sendMapDescription(pos); if (isLogin) { sendMagicEffect(pos, CONST_ME_TELEPORT); } for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { sendInventoryItem(static_cast(i), player->getInventoryItem(static_cast(i))); } sendStats(); sendSkills(); // gameworld light-settings sendWorldLight(g_game.getWorldLightInfo()); // player light level sendCreatureLight(creature); sendVIPEntries(); sendBasicData(); player->sendIcons(); } void ProtocolGame::sendMoveCreature(const Creature *creature, const Position &newPos, int32_t newStackPos, const Position &oldPos, int32_t oldStackPos, bool teleport) { if (creature == player) { if (oldStackPos >= 10) { sendMapDescription(newPos); } else if (teleport) { NetworkMessage msg; RemoveTileThing(msg, oldPos, oldStackPos); writeToOutputBuffer(msg); sendMapDescription(newPos); } else { NetworkMessage msg; if (oldPos.z == 7 && newPos.z >= 8) { RemoveTileThing(msg, oldPos, oldStackPos); } else { msg.addByte(0x6D); msg.addPosition(oldPos); msg.addByte(oldStackPos); msg.addPosition(newPos); if (otclientV8) { msg.add(creature->getStepDuration(true)); } } if (newPos.z > oldPos.z) { MoveDownCreature(msg, creature, newPos, oldPos); } else if (newPos.z < oldPos.z) { MoveUpCreature(msg, creature, newPos, oldPos); } if (oldPos.y > newPos.y) { // north, for old x msg.addByte(0x65); GetMapDescription(oldPos.x - awareRange.left(), newPos.y - awareRange.top(), newPos.z, awareRange.horizontal(), 1, msg); } else if (oldPos.y < newPos.y) { // south, for old x msg.addByte(0x67); GetMapDescription(oldPos.x - awareRange.left(), newPos.y + awareRange.bottom(), newPos.z, awareRange.horizontal(), 1, msg); } if (oldPos.x < newPos.x) { // east, [with new y] msg.addByte(0x66); GetMapDescription(newPos.x + awareRange.right(), newPos.y - awareRange.top(), newPos.z, 1, awareRange.vertical(), msg); } else if (oldPos.x > newPos.x) { // west, [with new y] msg.addByte(0x68); GetMapDescription(newPos.x - awareRange.left(), newPos.y - awareRange.top(), newPos.z, 1, awareRange.vertical(), msg); } writeToOutputBuffer(msg); } } else if (canSee(oldPos) && canSee(creature->getPosition())) { if (teleport || (oldPos.z == 7 && newPos.z >= 8) || oldStackPos >= 10) { sendRemoveTileThing(oldPos, oldStackPos); sendAddCreature(creature, newPos, newStackPos, false); } else { NetworkMessage msg; msg.addByte(0x6D); msg.addPosition(oldPos); msg.addByte(oldStackPos); msg.addPosition(creature->getPosition()); if (otclientV8) { msg.add(creature->getStepDuration(true)); } writeToOutputBuffer(msg); checkPredictiveWalking(oldPos); checkPredictiveWalking(newPos); } } else if (canSee(oldPos)) { sendRemoveTileThing(oldPos, oldStackPos); } else if (canSee(creature->getPosition())) { sendAddCreature(creature, newPos, newStackPos, false); } } void ProtocolGame::sendInventoryItem(slots_t slot, const Item *item) { NetworkMessage msg; if (item) { msg.addByte(0x78); msg.addByte(slot); msg.addItem(item, otclientV8); } else { msg.addByte(0x79); msg.addByte(slot); } writeToOutputBuffer(msg); } void ProtocolGame::sendItems() { NetworkMessage msg; msg.addByte(0xF5); const std::vector &inventory = Item::items.getInventory(); msg.add(inventory.size() + 11); for (uint16_t i = 1; i <= 11; i++) { msg.add(i); msg.addByte(0); // always 0 msg.add(1); // always 1 } for (auto clientId : inventory) { msg.add(clientId); msg.addByte(0); // always 0 msg.add(1); } writeToOutputBuffer(msg); } void ProtocolGame::sendAddContainerItem(uint8_t cid, uint16_t slot, const Item *item) { NetworkMessage msg; msg.addByte(0x70); msg.addByte(cid); msg.add(slot); msg.addItem(item, otclientV8); writeToOutputBuffer(msg); } void ProtocolGame::sendUpdateContainerItem(uint8_t cid, uint16_t slot, const Item *item) { NetworkMessage msg; msg.addByte(0x71); msg.addByte(cid); msg.add(slot); msg.addItem(item, otclientV8); writeToOutputBuffer(msg); } void ProtocolGame::sendRemoveContainerItem(uint8_t cid, uint16_t slot, const Item *lastItem) { NetworkMessage msg; msg.addByte(0x72); msg.addByte(cid); msg.add(slot); if (lastItem) { msg.addItem(lastItem, otclientV8); } else { msg.add(0x00); } writeToOutputBuffer(msg); } void ProtocolGame::sendTextWindow(uint32_t windowTextId, Item *item, uint16_t maxlen, bool canWrite) { NetworkMessage msg; msg.addByte(0x96); msg.add(windowTextId); msg.addItem(item, otclientV8); if (canWrite) { msg.add(maxlen); msg.addString(item->getText()); } else { const std::string &text = item->getText(); msg.add(text.size()); msg.addString(text); } const std::string &writer = item->getWriter(); if (!writer.empty()) { msg.addString(writer); } else { msg.add(0x00); } time_t writtenDate = item->getDate(); if (writtenDate != 0) { msg.addString(formatDateShort(writtenDate)); } else { msg.add(0x00); } writeToOutputBuffer(msg); } void ProtocolGame::sendTextWindow(uint32_t windowTextId, uint32_t itemId, const std::string &text) { NetworkMessage msg; msg.addByte(0x96); msg.add(windowTextId); msg.addItem(itemId, 1, otclientV8); msg.add(text.size()); msg.addString(text); msg.add(0x00); msg.add(0x00); writeToOutputBuffer(msg); } void ProtocolGame::sendHouseWindow(uint32_t windowTextId, const std::string &text) { NetworkMessage msg; msg.addByte(0x97); msg.addByte(0x00); msg.add(windowTextId); msg.addString(text); writeToOutputBuffer(msg); } void ProtocolGame::sendOutfitWindow() { NetworkMessage msg; msg.addByte(0xC8); Outfit_t currentOutfit = player->getDefaultOutfit(); Mount *currentMount = g_game.mounts.getMountByID(player->getCurrentMount()); if (currentMount) { currentOutfit.lookMount = currentMount->clientId; } const Outfit *outfitDef = Outfits::getInstance().getOutfitByLookType(player->getSex(), currentOutfit.lookType); if (outfitDef) { currentOutfit.attackRange = outfitDef->attackRange; for (int d = 0; d < OUTFIT_DIRECTIONS; ++d) { currentOutfit.offsets[d] = outfitDef->offsets[d]; } } AddOutfit(msg, currentOutfit, player->getAttackOutfitLookType()); std::vector protocolOutfits; if (player->isAccessPlayer()) { static const std::string gamemasterOutfitName = "Gamemaster"; protocolOutfits.emplace_back(gamemasterOutfitName, 75, 0); } const auto &outfits = Outfits::getInstance().getOutfits(player->getSex()); protocolOutfits.reserve(outfits.size()); for (const Outfit &outfit : outfits) { uint8_t addons; if (!player->getOutfitAddons(outfit, addons)) { continue; } protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons); if (protocolOutfits.size() == 500) { // Game client doesn't allow more than 500 outfits break; } } msg.addByte(protocolOutfits.size()); for (const ProtocolOutfit &outfit : protocolOutfits) { msg.add(outfit.lookType); msg.addString(outfit.name); msg.addByte(outfit.addons); } std::vector mounts; for (const Mount &mount : g_game.mounts.getMounts()) { if (player->hasMount(&mount)) { mounts.push_back(&mount); } } msg.addByte(mounts.size()); for (const Mount *mount : mounts) { msg.add(mount->clientId); msg.addString(mount->name); } if (otclientV8) { std::vector wings; for (const Wing &wing : g_game.wings.getWings()) { if (player->hasWing(&wing)) { wings.push_back(&wing); } } msg.addByte(wings.size()); for (const Wing *wing : wings) { msg.add(wing->clientId); msg.addString(wing->name); } std::vector auras; for (const Aura &aura : g_game.auras.getAuras()) { if (player->hasAura(&aura)) { auras.push_back(&aura); } } msg.addByte(auras.size()); for (const Aura *aura : auras) { msg.add(aura->clientId); msg.addString(aura->name); } std::vector shaders; for (const Shader &shader : g_game.shaders.getShaders()) { if (player->hasShader(&shader)) { shaders.push_back(&shader); } } msg.addByte(shaders.size()); for (const Shader *shader : shaders) { msg.add(shader->id); msg.addString(shader->name); } std::vector runeEffectList; for (const RuneEffect &re : g_game.runeEffects.getRuneEffects()) { if (player->hasRuneEffect(&re)) { runeEffectList.push_back(&re); } } msg.addByte(runeEffectList.size()); for (const RuneEffect *re : runeEffectList) { msg.add(re->id); msg.addString(re->name); msg.addByte(static_cast(std::min(255, static_cast(re->opacity * 255)))); msg.addByte(static_cast(std::min(255, static_cast(re->speed * 100)))); msg.addByte(static_cast(std::min(255, static_cast(re->scale * 100)))); // color RGB uint8_t cr = 0xFF, cg = 0xFF, cb = 0xFF; if (re->color.size() >= 7 && re->color[0] == '#') { cr = static_cast(std::stoul(re->color.substr(1, 2), nullptr, 16)); cg = static_cast(std::stoul(re->color.substr(3, 2), nullptr, 16)); cb = static_cast(std::stoul(re->color.substr(5, 2), nullptr, 16)); } msg.addByte(cr); msg.addByte(cg); msg.addByte(cb); msg.addByte(static_cast(std::min(255, static_cast(re->flicker * 255)))); msg.addByte(static_cast(std::min(255, static_cast(re->colorShift * 255)))); msg.addByte(re->layer == "front" ? 1 : 0); msg.add(re->bonusLoot); msg.add(re->bonusDamage); msg.add(re->bonusSpeed); msg.add(re->bonusXp); msg.add(static_cast(std::min(65535, re->damageInterval))); msg.add(static_cast(std::min(65535, re->damageDelay))); // Multi-layer data uint8_t layerCount = static_cast(std::min(255, re->layers.size())); msg.addByte(layerCount); for (const RuneEffectLayer &rl : re->layers) { msg.add(rl.effectId); msg.add(static_cast(std::max(-32768, std::min(32767, static_cast(rl.offsetX * 10))))); msg.add(static_cast(std::max(-32768, std::min(32767, static_cast(rl.offsetY * 10))))); msg.addByte(static_cast(std::min(255, static_cast(rl.speed * 100)))); msg.add(static_cast(std::min(65535, static_cast(rl.scale * 100)))); msg.addByte(static_cast(std::min(255, static_cast(rl.opacity * 255)))); uint8_t lcr = 0xFF, lcg = 0xFF, lcb = 0xFF; if (rl.color.size() >= 7 && rl.color[0] == '#') { lcr = static_cast(std::stoul(rl.color.substr(1, 2), nullptr, 16)); lcg = static_cast(std::stoul(rl.color.substr(3, 2), nullptr, 16)); lcb = static_cast(std::stoul(rl.color.substr(5, 2), nullptr, 16)); } msg.addByte(lcr); msg.addByte(lcg); msg.addByte(lcb); msg.addByte(static_cast(std::min(255, static_cast(rl.flicker * 255)))); msg.addByte(static_cast(std::min(255, static_cast(rl.colorShift * 255)))); msg.addByte(static_cast(std::min(255, static_cast(rl.drawOrder)))); msg.addByte(rl.behind ? 1 : 0); bool hasTexture = !rl.texture.empty(); msg.addByte(hasTexture ? 1 : 0); if (hasTexture) { msg.addString(rl.texture); } } } } writeToOutputBuffer(msg); } void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus) { NetworkMessage msg; msg.addByte(0xD3); msg.add(guid); msg.addByte(newStatus); writeToOutputBuffer(msg); } void ProtocolGame::sendVIP(uint32_t guid, const std::string &name, const std::string &description, uint32_t icon, bool notify, VipStatus_t status) { NetworkMessage msg; msg.addByte(0xD2); msg.add(guid); msg.addString(name); msg.addString(description); msg.add(std::min(10, icon)); msg.addByte(notify ? 0x01 : 0x00); msg.addByte(status); writeToOutputBuffer(msg); } void ProtocolGame::sendVIPEntries() { const std::forward_list &vipEntries = IOLoginData::getVIPEntries(player->getAccount()); for (const VIPEntry &entry : vipEntries) { VipStatus_t vipStatus = VIPSTATUS_ONLINE; Player *vipPlayer = g_game.getPlayerByGUID(entry.guid); if (!vipPlayer || vipPlayer->isInGhostMode() || player->isAccessPlayer()) { vipStatus = VIPSTATUS_OFFLINE; } sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus); } } void ProtocolGame::sendSpellCooldown(uint8_t spellId, uint32_t time) { NetworkMessage msg; msg.addByte(0xA4); msg.addByte(spellId); msg.add(time); writeToOutputBuffer(msg); } void ProtocolGame::sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time) { NetworkMessage msg; msg.addByte(0xA5); msg.addByte(groupId); msg.add(time); writeToOutputBuffer(msg); } void ProtocolGame::sendModalWindow(const ModalWindow &modalWindow) { NetworkMessage msg; msg.addByte(0xFA); msg.add(modalWindow.id); msg.addString(modalWindow.title); msg.addString(modalWindow.message); msg.addByte(modalWindow.buttons.size()); for (const auto &it : modalWindow.buttons) { msg.addString(it.first); msg.addByte(it.second); } msg.addByte(modalWindow.choices.size()); for (const auto &it : modalWindow.choices) { msg.addString(it.first); msg.addByte(it.second); } msg.addByte(modalWindow.defaultEscapeButton); msg.addByte(modalWindow.defaultEnterButton); msg.addByte(modalWindow.priority ? 0x01 : 0x00); writeToOutputBuffer(msg); } ////////////// Add common messages void ProtocolGame::AddCreature(NetworkMessage &msg, const Creature *creature, bool known, uint32_t remove) { CreatureType_t creatureType = creature->getType(); const Player *otherPlayer = creature->getPlayer(); if (known) { msg.add(0x62); msg.add(creature->getID()); } else { msg.add(0x61); msg.add(remove); msg.add(creature->getID()); msg.addByte(creatureType); msg.addString(creature->getName()); } if (creature->isHealthHidden()) { msg.addByte(0x00); } else { msg.addByte(std::ceil((static_cast(creature->getHealth()) / std::max(creature->getMaxHealth(), 1)) * 100)); } msg.addByte(creature->getDirection()); if (!creature->isInGhostMode() && !creature->isInvisible()) { Outfit_t creatureOutfit = creature->getCurrentOutfit(); const Player *creaturePlayer = creature->getPlayer(); if (creaturePlayer) { const Outfit *outfitDef = Outfits::getInstance().getOutfitByLookType(creaturePlayer->getSex(), creatureOutfit.lookType); if (outfitDef) { creatureOutfit.attackRange = outfitDef->attackRange; for (int d = 0; d < OUTFIT_DIRECTIONS; ++d) { creatureOutfit.offsets[d] = outfitDef->offsets[d]; } } } AddOutfit(msg, creatureOutfit, creature->getAttackOutfitLookType()); } else { static Outfit_t outfit; AddOutfit(msg, outfit, creature->getAttackOutfitLookType()); } LightInfo lightInfo = creature->getCreatureLight(); msg.addByte(lightInfo.level); msg.addByte(lightInfo.color); msg.add(creature->getStepSpeed() / 2); msg.addByte(player->getSkullClient(creature)); msg.addByte(player->getPartyShield(otherPlayer)); if (!known) { msg.addByte(player->getGuildEmblem(otherPlayer)); } if (creatureType == CREATURETYPE_MONSTER) { const Creature *master = creature->getMaster(); if (master) { const Player *masterPlayer = master->getPlayer(); if (masterPlayer) { if (masterPlayer == player) { creatureType = CREATURETYPE_SUMMON_OWN; } else { creatureType = CREATURETYPE_SUMMON_OTHERS; } } } } msg.addByte(creatureType); // Type (for summons) msg.addByte(creature->getSpeechBubble()); msg.addByte(0xFF); // MARK_UNMARKED if (otherPlayer) { msg.add(otherPlayer->getHelpers()); } else { msg.add(0x00); } msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01); } void ProtocolGame::AddPlayerStats(NetworkMessage &msg) { msg.addByte(0xA0); msg.add(std::min(player->getHealth(), std::numeric_limits::max())); msg.add(std::min(player->getMaxHealth(), std::numeric_limits::max())); msg.add(player->getFreeCapacity()); msg.add(player->getCapacity()); msg.add(player->getExperience()); msg.add(player->getLevel()); msg.addByte(player->getLevelPercent()); msg.add(100); // base xp gain rate msg.add(0); // xp voucher msg.add(0); // low level bonus msg.add(0); // xp boost msg.add(100); // stamina multiplier (100 = x1.0) msg.add(std::min(player->getMana(), std::numeric_limits::max())); msg.add(std::min(player->getMaxMana(), std::numeric_limits::max())); msg.addByte(std::min(player->getMagicLevel(), std::numeric_limits::max())); msg.addByte(std::min(player->getBaseMagicLevel(), std::numeric_limits::max())); msg.addByte(player->getMagicLevelPercent()); msg.addByte(player->getSoul()); msg.add(player->getStaminaMinutes()); msg.add(player->getBaseSpeed() / 2); Condition *condition = player->getCondition(CONDITION_REGENERATION); msg.add(condition ? condition->getTicks() / 1000 : 0x00); msg.add(player->getOfflineTrainingTime() / 60 / 1000); msg.add(0); // xp boost time (seconds) msg.addByte(0); // enables exp boost in the store } void ProtocolGame::AddPlayerSkills(NetworkMessage &msg) { msg.addByte(0xA1); for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { msg.add(std::min(player->getSkillLevel(i), std::numeric_limits::max())); msg.add(player->getBaseSkill(i)); msg.addByte(player->getSkillPercent(i)); } for (uint8_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { msg.add(std::min(100, player->varSpecialSkills[i])); msg.add(0); } } void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, uint16_t lookAttack) { msg.add(outfit.lookType); if (outfit.lookType != 0) { msg.addByte(outfit.lookHead); msg.addByte(outfit.lookBody); msg.addByte(outfit.lookLegs); msg.addByte(outfit.lookFeet); msg.addByte(outfit.lookAddons); msg.add(lookAttack); } else { msg.addItemId(outfit.lookTypeEx); } msg.add(outfit.lookMount); if (otclientV8) { msg.add(outfit.lookWings); msg.add(outfit.lookAura); Shader *shader = g_game.shaders.getShaderByID(outfit.lookShader); msg.addString(shader ? shader->name : ""); msg.add(outfit.lookRuneEffect); msg.addByte(outfit.attackRange); for (int d = 0; d < OUTFIT_DIRECTIONS; ++d) { msg.add(outfit.offsets[d].lookX); msg.add(outfit.offsets[d].lookY); msg.add(outfit.offsets[d].meleeX); msg.add(outfit.offsets[d].meleeY); msg.add(outfit.offsets[d].rangeX); msg.add(outfit.offsets[d].rangeY); } } } void ProtocolGame::AddWorldLight(NetworkMessage &msg, LightInfo lightInfo) { msg.addByte(0x82); msg.addByte(lightInfo.level); msg.addByte(lightInfo.color); } void ProtocolGame::AddCreatureLight(NetworkMessage &msg, const Creature *creature) { LightInfo lightInfo = creature->getCreatureLight(); msg.addByte(0x8D); msg.add(creature->getID()); msg.addByte(lightInfo.level); msg.addByte(lightInfo.color); } // tile void ProtocolGame::RemoveTileThing(NetworkMessage &msg, const Position &pos, uint32_t stackpos) { if (stackpos >= 10) { return; } msg.addByte(0x6C); msg.addPosition(pos); msg.addByte(stackpos); } void ProtocolGame::MoveUpCreature(NetworkMessage &msg, const Creature *creature, const Position &newPos, const Position &oldPos) { if (creature != player) { return; } // floor change up msg.addByte(0xBE); // going to surface if (newPos.z == 7) { int32_t skip = -1; if (otclientV8) { for (int z = 5; z >= 0; --z) { sendFloorDescription(oldPos, z); } } else { GetFloorDescription(msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 5, awareRange.horizontal(), awareRange.vertical(), 3, skip); //(floor 7 and 6 already set) GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 4, awareRange.horizontal(), awareRange.vertical(), 4, skip); GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 3, awareRange.horizontal(), awareRange.vertical(), 5, skip); GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 2, awareRange.horizontal(), awareRange.vertical(), 6, skip); GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 1, awareRange.horizontal(), awareRange.vertical(), 7, skip); GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), 0, awareRange.horizontal(), awareRange.vertical(), 8, skip); if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } } } // underground, going one floor up (still underground) else if (newPos.z > 7) { int32_t skip = -1; GetFloorDescription(msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), oldPos.getZ() - 6, awareRange.horizontal(), awareRange.vertical(), 6, skip); if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } } // moving up a floor up makes us out of sync // west msg.addByte(0x68); GetMapDescription(oldPos.x - awareRange.left(), oldPos.y - (awareRange.top() - 1), newPos.z, 1, awareRange.vertical(), msg); // north msg.addByte(0x65); GetMapDescription(oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), newPos.z, awareRange.horizontal(), 1, msg); } void ProtocolGame::MoveDownCreature(NetworkMessage &msg, const Creature *creature, const Position &newPos, const Position &oldPos) { if (creature != player) { return; } // floor change down msg.addByte(0xBF); // going from surface to underground if (newPos.z == 8) { int32_t skip = -1; if (otclientV8) { for (int z = 0; z < 3; ++z) { sendFloorDescription(oldPos, newPos.z + z); } } else { GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), newPos.z, awareRange.horizontal(), awareRange.vertical(), -1, skip); GetFloorDescription(msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), newPos.z + 1, awareRange.horizontal(), awareRange.vertical(), -2, skip); GetFloorDescription(msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), newPos.z + 2, awareRange.horizontal(), awareRange.vertical(), -3, skip); if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } } } // going further down else if (newPos.z > oldPos.z && newPos.z > 8 && newPos.z < 11) { int32_t skip = -1; GetFloorDescription( msg, oldPos.x - awareRange.left(), oldPos.y - awareRange.top(), newPos.z + 5, awareRange.horizontal(), awareRange.vertical(), -6, skip); if (skip >= 0) { msg.addByte(skip); msg.addByte(0xFF); } } // moving down a floor makes us out of sync // east msg.addByte(0x66); GetMapDescription(newPos.x + awareRange.right(), newPos.y - awareRange.top(), newPos.z, 1, awareRange.vertical(), msg); // south msg.addByte(0x67); GetMapDescription(newPos.x - awareRange.left(), newPos.y - awareRange.top(), newPos.z, 1, awareRange.vertical(), msg); } void ProtocolGame::AddShopItem(NetworkMessage &msg, const ShopInfo &item) { const ItemType &it = Item::items[item.itemId]; msg.add(it.clientId); if (it.isSplash() || it.isFluidContainer()) { msg.addByte(serverFluidToClient(item.subType)); } else { msg.addByte(0x00); } msg.addString(item.realName); msg.add(it.weight); msg.add(item.buyPrice); msg.add(item.sellPrice); } void ProtocolGame::parseExtendedOpcode(NetworkMessage &msg) { uint8_t opcode = msg.getByte(); const std::string &buffer = msg.getString(); // process additional opcodes via lua script event addGameTask(&Game::parsePlayerExtendedOpcode, player->getID(), opcode, buffer); } // OTCv8 void ProtocolGame::sendFeatures() { if (!otclientV8) return; std::map features; // place for non-standard OTCv8 features features[GameExtendedOpcode] = true; features[GameChangeMapAwareRange] = true; features[GameNewWalking] = true; features[GameEnvironmentEffect] = false; // disable it, useless 2 byte with every tile features[GameExtendedClientPing] = true; features[GameItemTooltip] = true; // fully available from version 2.6 features[GameWingsAndAura] = true; features[GameOutfitShaders] = true; // packet compression // we don't send feature, because feature assumes all packets are compressed // if adler32 is enabled then compression can be detected automaticly, just // adlre32 must be 0 if (g_config.getBoolean(ConfigManager::PACKET_COMPRESSION)) { enableCompression(); } if (features.empty()) return; auto msg = getOutputBuffer(1024); msg->addByte(0x43); msg->add(features.size()); for (auto &feature : features) { msg->addByte((uint8_t)feature.first); msg->addByte(feature.second ? 1 : 0); } send(std::move(getCurrentBuffer())); // send this packet immediately } void ProtocolGame::parseChangeAwareRange(NetworkMessage &msg) { uint8_t width = msg.get(); uint8_t height = msg.get(); g_dispatcher.addTask(createTask( std::bind(&ProtocolGame::updateAwareRange, getThis(), width, height))); } void ProtocolGame::updateAwareRange(int width, int height) { if (!otclientV8) return; // If you want to change max awareRange, edit maxViewportX, maxViewportY, // maxClientViewportX, maxClientViewportY in map.h awareRange.width = std::min(Map::maxViewportX * 2 - 1, std::min(Map::maxClientViewportX * 2 + 1, std::max(15, width))); awareRange.height = std::min(Map::maxViewportY * 2 - 1, std::min(Map::maxClientViewportY * 2 + 1, std::max(11, height))); // numbers must be odd if (awareRange.width % 2 != 1) awareRange.width -= 1; if (awareRange.height % 2 != 1) awareRange.height -= 1; sendAwareRange(); sendMapDescription(player->getPosition()); // refresh map } void ProtocolGame::sendAwareRange() { if (!otclientV8) return; NetworkMessage msg; msg.addByte(0x42); msg.add(awareRange.width); msg.add(awareRange.height); writeToOutputBuffer(msg); } void ProtocolGame::parseNewWalking(NetworkMessage &msg) { uint32_t playerWalkId = msg.get(); int32_t predictiveWalkId = msg.get(); // extension for proxy system, currently not used Position playerPosition = msg.getPosition(); // local player position before // moving, including prewalk uint8_t flags = msg.getByte(); // 0x01 - prewalk, 0x02 - autowalk uint16_t numdirs = msg.get(); if (numdirs == 0 || numdirs > 4096) { return; } std::list path; for (uint16_t i = 0; i < numdirs; ++i) { uint8_t rawdir = msg.getByte(); switch (rawdir) { case 1: path.push_back(DIRECTION_EAST); break; case 2: path.push_back(DIRECTION_NORTHEAST); break; case 3: path.push_back(DIRECTION_NORTH); break; case 4: path.push_back(DIRECTION_NORTHWEST); break; case 5: path.push_back(DIRECTION_WEST); break; case 6: path.push_back(DIRECTION_SOUTHWEST); break; case 7: path.push_back(DIRECTION_SOUTH); break; case 8: path.push_back(DIRECTION_SOUTHEAST); break; default: break; } } uint32_t playerId = player->getID(); auto self(getThis()); g_dispatcher.addTask(createTask([self, playerWalkId, predictiveWalkId, playerId, playerPosition, flags, path] { bool preWalk = flags & 0x01; Position destination = getNextPosition(*(path.begin()), playerPosition); if (preWalk && predictiveWalkId < self->walkMatrix.get(destination)) { self->walkId += 1; self->sendWalkId(); return; } if (playerWalkId < self->walkId) { // this walk has been sent before player received previous newCancelWalk, // so it's invalid, ignore it return; } g_game.playerNewWalk(playerId, playerPosition, flags, path); })); } void ProtocolGame::checkPredictiveWalking(const Position &pos) { if (!otclientV8) return; if (walkMatrix.inRange(pos) && !g_game.map.canWalkTo(*player, pos)) { int newValue = walkMatrix.update(pos); sendPredictiveCancel(pos, newValue); } } void ProtocolGame::sendPredictiveCancel(const Position &pos, int value) { (void)value; if (!otclientV8) return; NetworkMessage msg; msg.addByte(0x46); msg.addPosition(pos); msg.addByte(player->getDirection()); writeToOutputBuffer(msg); } void ProtocolGame::sendWalkId() { if (!otclientV8) return; NetworkMessage msg; msg.addByte(0x47); msg.add(walkId); writeToOutputBuffer(msg); } void ProtocolGame::sendNewCancelWalk() { if (!otclientV8) return; NetworkMessage msg; msg.addByte(0x45); msg.addByte(player->getDirection()); writeToOutputBuffer(msg); walkId += 1; sendWalkId(); } void ProtocolGame::parseNewPing(NetworkMessage &msg) { uint32_t pingId = msg.get(); uint16_t localPing = msg.get(); uint16_t fps = msg.get(); addGameTask(&Game::playerReceiveNewPing, player->getID(), localPing, fps); g_dispatcher.addTask( createTask(std::bind(&ProtocolGame::sendNewPing, getThis(), pingId))); } void ProtocolGame::sendNewPing(uint32_t pingId) { if (!otclientV8) return; NetworkMessage msg; msg.addByte(0x40); msg.add(pingId); writeToOutputBuffer(msg); }