refactor: world packets as structs

WorldPackets now holds what a client sends a world server, one struct per
packet with Serialize/Deserialize: Validation, CharacterListRequest,
CharacterCreateRequest, CharacterLoginRequest, GameMessage,
CharacterDeleteRequest, CharacterRenameRequest, LevelLoadComplete,
PositionUpdate, MailPacket, RoutePacket, StringCheck, GeneralChatMessage,
HandleFunness, UIHelpTop5. WorldServer's switch became a dispatch map of
handlers (each overrides Handle in WorldServer.cpp, logic unchanged:
dashboard hooks, LoadPlayer, chat logging, migration checks, message
inspector capture all still run); a packet that does not deserialize is
logged and dropped. UserManager's create/delete/rename take the structs.

The answers are ClientPackets (the CLIENT service): LoadStaticZone,
CharacterListResponse, CharacterCreateResponse, CharacterRenameResponse,
DeleteCharacterResponse, TransferToWorld, ServerStates, CreateCharacter,
ChatModerationString, MakeGMResponse, HTTPMonitorInfoResponse and
DebugOutput, built at the call sites (world server, UserManager, slash
commands, dashboard actions, components, migration). The world -> chat
forward of a routed packet is ChatPackets::RoutedFromClient. All
WorldPackets::Send* functions, HTTPMonitorInfo and the ClientPackets parse
functions are gone. The architecture doc now states the file rule: a
packet lives in the file of the ServiceType in its header.

No wire change. Verified against frozen verbatim copies of the old code
(tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h): every response
is sent through the old function and the struct over grids of inputs
(all enum values, strings of every length class, IDs, >64 moderation
segments, big XML) and must match byte for byte; every request is read
by the old code and the struct and must give the same values; plus
golden bytes, round trips, truncation checks and a field width mutation
that made the tests fail. Only differences: malformed requests are
dropped instead of handled with partial values, and LevelLoadComplete now
reads the zone ID the client sends after it (lu_packets and captures show
the 1.10.64 client always sends it; DLU ignored it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:10:57 -05:00
parent efa08ac1d0
commit b489ee59f8
21 changed files with 3040 additions and 907 deletions

View File

@@ -10,6 +10,8 @@
#include "Logger.h"
#include "User.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "ClientPackets.h"
#include "Character.h"
#include "BitStream.h"
#include "ObjectIDManager.h"
@@ -244,89 +246,59 @@ void UserManager::RequestCharacterList(const SystemAddress& sysAddr) {
chars.push_back(character);
}
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE);
ClientPackets::CharacterListResponse response;
response.selectedCharacterIndex = 0; //TODO: Pick the most recent played index. character index in front, just picking 0
std::vector<Character*> characters = u->GetCharacters();
bitStream.Write<uint8_t>(characters.size());
bitStream.Write<uint8_t>(0); //TODO: Pick the most recent played index. character index in front, just picking 0
for (const auto* const character : u->GetCharacters()) {
auto& entry = response.characters.emplace_back();
entry.objectID = character->GetObjectID();
for (uint32_t i = 0; i < characters.size(); ++i) {
bitStream.Write(characters[i]->GetObjectID());
bitStream.Write<uint32_t>(0);
entry.name = LUWString(character->GetName());
entry.unapprovedName = LUWString(character->GetUnapprovedName());
bitStream.Write(LUWString(characters[i]->GetName()));
bitStream.Write(LUWString(characters[i]->GetUnapprovedName()));
entry.nameRejected = character->GetNameRejected();
bitStream.Write<uint8_t>(characters[i]->GetNameRejected());
bitStream.Write<uint8_t>(false);
entry.shirtColor = character->GetShirtColor();
entry.shirtStyle = character->GetShirtStyle();
entry.pantsColor = character->GetPantsColor();
entry.hairStyle = character->GetHairStyle();
entry.hairColor = character->GetHairColor();
entry.leftHand = character->GetLeftHand();
entry.rightHand = character->GetRightHand();
entry.eyebrows = character->GetEyebrows();
entry.eyes = character->GetEyes();
entry.mouth = character->GetMouth();
bitStream.Write(LUString("", 10));
entry.zoneID = character->GetZoneID();
entry.zoneInstance = character->GetZoneInstance();
entry.zoneClone = character->GetZoneClone();
bitStream.Write(characters[i]->GetShirtColor());
bitStream.Write(characters[i]->GetShirtStyle());
bitStream.Write(characters[i]->GetPantsColor());
bitStream.Write(characters[i]->GetHairStyle());
bitStream.Write(characters[i]->GetHairColor());
bitStream.Write(characters[i]->GetLeftHand());
bitStream.Write(characters[i]->GetRightHand());
bitStream.Write(characters[i]->GetEyebrows());
bitStream.Write(characters[i]->GetEyes());
bitStream.Write(characters[i]->GetMouth());
bitStream.Write<uint32_t>(0);
entry.lastLogin = character->GetLastLogin();
bitStream.Write<uint16_t>(characters[i]->GetZoneID());
bitStream.Write<uint16_t>(characters[i]->GetZoneInstance());
bitStream.Write(characters[i]->GetZoneClone());
bitStream.Write(characters[i]->GetLastLogin());
const auto& equippedItems = characters[i]->GetEquippedItems();
bitStream.Write<uint16_t>(equippedItems.size());
for (uint32_t j = 0; j < equippedItems.size(); ++j) {
bitStream.Write(equippedItems[j]);
}
entry.equippedItems = character->GetEquippedItems();
}
SEND_PACKET;
response.Send(sysAddr);
}
void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) {
void UserManager::CreateCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterCreateRequest& request) {
User* u = GetUser(sysAddr);
if (!u) return;
LUWString LUWStringName;
uint32_t firstNameIndex;
uint32_t middleNameIndex;
uint32_t lastNameIndex;
uint32_t shirtColor;
uint32_t shirtStyle;
uint32_t pantsColor;
uint32_t hairStyle;
uint32_t hairColor;
uint32_t lh;
uint32_t rh;
uint32_t eyebrows;
uint32_t eyes;
uint32_t mouth;
CINSTREAM_SKIP_HEADER;
inStream.Read(LUWStringName);
inStream.Read(firstNameIndex);
inStream.Read(middleNameIndex);
inStream.Read(lastNameIndex);
inStream.IgnoreBytes(9);
inStream.Read(shirtColor);
inStream.Read(shirtStyle);
inStream.Read(pantsColor);
inStream.Read(hairStyle);
inStream.Read(hairColor);
inStream.Read(lh);
inStream.Read(rh);
inStream.Read(eyebrows);
inStream.Read(eyes);
inStream.Read(mouth);
const auto& LUWStringName = request.name;
const uint32_t firstNameIndex = request.firstNameIndex;
const uint32_t middleNameIndex = request.middleNameIndex;
const uint32_t lastNameIndex = request.lastNameIndex;
const uint32_t shirtColor = request.shirtColor;
const uint32_t shirtStyle = request.shirtStyle;
const uint32_t pantsColor = request.pantsColor;
const uint32_t hairStyle = request.hairStyle;
const uint32_t hairColor = request.hairColor;
const uint32_t lh = request.leftHand;
const uint32_t rh = request.rightHand;
const uint32_t eyebrows = request.eyebrows;
const uint32_t eyes = request.eyes;
const uint32_t mouth = request.mouth;
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
@@ -338,13 +310,17 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
if (!name.empty() && Database::Get()->IsNameInUse(name)) {
LOG("AccountID: %i chose unavailable name: %s", u->GetAccountID(), name.c_str());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::CUSTOM_NAME_IN_USE);
ClientPackets::CharacterCreateResponse response;
response.response = eCharacterCreationResponse::CUSTOM_NAME_IN_USE;
response.Send(sysAddr);
return;
}
if (Database::Get()->IsNameInUse(predefinedName)) {
LOG("AccountID: %i chose unavailable predefined name: %s", u->GetAccountID(), predefinedName.c_str());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::PREDEFINED_NAME_IN_USE);
ClientPackets::CharacterCreateResponse response;
response.response = eCharacterCreationResponse::PREDEFINED_NAME_IN_USE;
response.Send(sysAddr);
return;
}
@@ -370,7 +346,9 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
if (tries >= maxRetries) {
LOG("Failed to get a unique objectID for new character after %i tries, aborting char creation for account %i", maxRetries, u->GetAccountID());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE);
ClientPackets::CharacterCreateResponse response;
response.response = eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE;
response.Send(sysAddr);
return;
}
@@ -430,20 +408,20 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
//Now finally insert our character xml:
Database::Get()->InsertCharacterXml(objectID, xml.str());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::SUCCESS);
ClientPackets::CharacterCreateResponse response;
response.response = eCharacterCreationResponse::SUCCESS;
response.Send(sysAddr);
UserManager::RequestCharacterList(sysAddr);
}
void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) {
void UserManager::DeleteCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterDeleteRequest& request) {
User* u = GetUser(sysAddr);
if (!u) {
LOG("Couldn't get user to delete character");
return;
}
CINSTREAM_SKIP_HEADER;
LWOOBJID objectID;
inStream.Read(objectID);
const LWOOBJID objectID = request.objectID;
LOG("Received char delete req for ID: %llu", objectID);
@@ -455,7 +433,9 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet)
u->GetAccountID());
if (!hasCharacter) {
WorldPackets::SendCharacterDeleteResponse(sysAddr, false);
ClientPackets::DeleteCharacterResponse response;
response.success = false;
response.Send(sysAddr);
} else {
LOG("Deleting character %llu", objectID);
Database::Get()->DeleteCharacter(objectID);
@@ -466,26 +446,24 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet)
bitStream.Write(objectID);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
WorldPackets::SendCharacterDeleteResponse(sysAddr, true);
ClientPackets::DeleteCharacterResponse response;
response.success = true;
response.Send(sysAddr);
}
}
void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) {
void UserManager::RenameCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterRenameRequest& request) {
User* u = GetUser(sysAddr);
if (!u) {
LOG("Couldn't get user to delete character");
return;
}
CINSTREAM_SKIP_HEADER;
LWOOBJID objectID;
inStream.Read(objectID);
const LWOOBJID objectID = request.objectID;
LOG("Received char rename request for ID: %llu", objectID);
LUWString LUWStringName;
inStream.Read(LUWStringName);
auto newName = LUWStringName.GetAsString();
auto newName = request.name.GetAsString();
Character* character = nullptr;
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
@@ -507,7 +485,9 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
});
if (!ownsCharacter || !character) {
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::UNKNOWN_ERROR;
response.Send(sysAddr);
} else if (ownsCharacter && character) {
if (autoRejectNames) {
// Create a random preapproved name (fallback to default if none available)
@@ -522,7 +502,9 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
}
if (newName == character->GetName()) {
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::NAME_UNAVAILABLE;
response.Send(sysAddr);
return;
}
@@ -531,28 +513,38 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
Database::Get()->SetCharacterName(objectID, newName);
DashboardNotify::Changed("characters", objectID);
LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str());
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::SUCCESS;
response.Send(sysAddr);
UserManager::RequestCharacterList(sysAddr);
} else if (IsNamePreapproved(newName)) {
Database::Get()->SetCharacterName(objectID, newName);
DashboardNotify::Changed("characters", objectID);
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::SUCCESS;
response.Send(sysAddr);
UserManager::RequestCharacterList(sysAddr);
} else {
Database::Get()->SetPendingCharacterName(objectID, newName);
DashboardNotify::Changed("characters", objectID);
DashboardNotify::Changed("pending_names", objectID);
LOG("Character %s has been renamed to %s and is pending approval by a moderator.", character->GetName().c_str(), newName.c_str());
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::SUCCESS;
response.Send(sysAddr);
UserManager::RequestCharacterList(sysAddr);
}
} else {
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_IN_USE);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::NAME_IN_USE;
response.Send(sysAddr);
}
} else {
LOG("Unknown error occurred when renaming character, either hasCharacter or character variable != true.");
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
ClientPackets::CharacterRenameResponse response;
response.response = eRenameResponse::UNKNOWN_ERROR;
response.Send(sysAddr);
}
}
@@ -591,7 +583,11 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, LWOOBJID playerID
character->SetZoneInstance(zoneInstance);
character->SetZoneClone(zoneClone);
}
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
return;
});
} else {

View File

@@ -9,6 +9,12 @@
class User;
namespace WorldPackets {
struct CharacterCreateRequest;
struct CharacterDeleteRequest;
struct CharacterRenameRequest;
}
class UserManager {
public:
static UserManager* Instance() {
@@ -33,9 +39,9 @@ public:
bool IsNamePreapproved(const std::string& requestedName);
void RequestCharacterList(const SystemAddress& sysAddr);
void CreateCharacter(const SystemAddress& sysAddr, Packet* packet);
void DeleteCharacter(const SystemAddress& sysAddr, Packet* packet);
void RenameCharacter(const SystemAddress& sysAddr, Packet* packet);
void CreateCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterCreateRequest& request);
void DeleteCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterDeleteRequest& request);
void RenameCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterRenameRequest& request);
void LoginCharacter(const SystemAddress& sysAddr, LWOOBJID playerID);
void SaveAllActiveCharacters();

View File

@@ -5,7 +5,7 @@
#include "Character.h"
#include "CharacterComponent.h"
#include "ChatPackets.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "EntityManager.h"
#include "Game.h"
#include "ZoneInstanceManager.h"
@@ -55,7 +55,11 @@ void PropertyTeleportBehavior::Handle(BehaviorContext* context, RakNet::BitStrea
entity->GetCharacter()->SaveXMLToDatabase();
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
return;
});
}

View File

@@ -7,7 +7,7 @@
#include "ZoneInstanceManager.h"
#include "Game.h"
#include "Logger.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "EntityManager.h"
#include "ChatPackets.h"
#include "BitStreamUtils.h"
@@ -477,7 +477,11 @@ void ActivityInstance::StartZone() {
player->GetCharacter()->SetZoneClone(zoneClone);
}
WorldPackets::SendTransferToWorld(player->GetSystemAddress(), serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(player->GetSystemAddress());
return;
});
}

View File

@@ -22,7 +22,7 @@
#include "CDRewardCodesTable.h"
#include "Mail.h"
#include "ZoneInstanceManager.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "MessageType/Game.h"
#include <ctime>
#include <ranges>
@@ -899,7 +899,11 @@ void CharacterComponent::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) const {
character->SaveXMLToDatabase();
}
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
Game::entityManager->DestructEntity(entity);
});

View File

@@ -20,7 +20,7 @@
#include "CppScripts.h"
#include "UserManager.h"
#include "ZoneInstanceManager.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "Item.h"
#include "ZCompression.h"
#include "dConfig.h"
@@ -4414,7 +4414,11 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream& inStream, Entity
character->SetZoneClone(zoneClone);
}
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
return;
});
}

View File

@@ -14,7 +14,7 @@
#include "PlayerManager.h"
#include "User.h"
#include "UserManager.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "ZoneInstanceManager.h"
#include "eGameMasterLevel.h"
#include "eServerDisconnectIdentifiers.h"
@@ -58,7 +58,12 @@ namespace {
auto* entity = PlayerManager::GetPlayer(user->GetSystemAddress());
if (!entity || entity->GetGMLevel() <= info->maxGmLevel) continue;
WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), true, info->maxGmLevel, entity->GetGMLevel(), info->maxGmLevel);
ClientPackets::MakeGMResponse response;
response.success = true;
response.highestLevel = info->maxGmLevel;
response.previousLevel = entity->GetGMLevel();
response.newLevel = info->maxGmLevel;
response.Send(entity->GetSystemAddress());
GameMessages::SendChatModeUpdate(entity->GetObjectID(), info->maxGmLevel);
entity->SetGMLevel(info->maxGmLevel);
if (info->maxGmLevel == eGameMasterLevel::CIVILIAN) {
@@ -138,7 +143,11 @@ namespace {
character->SaveXMLToDatabase();
LOG("Dashboard rescue: transferring %llu to zone %u", objectId, zoneID);
WorldPackets::SendTransferToWorld(entity->GetSystemAddress(), serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(entity->GetSystemAddress());
});
return 1;
}

View File

@@ -20,7 +20,7 @@
#include "UserManager.h"
#include "User.h"
#include "VanityUtilities.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "ZoneInstanceManager.h"
// Database
@@ -80,7 +80,12 @@ namespace DEVGMCommands {
bool success = user->GetMaxGMLevel() >= level;
if (success) {
WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), success, user->GetMaxGMLevel(), entity->GetGMLevel(), level);
ClientPackets::MakeGMResponse response;
response.success = success;
response.highestLevel = user->GetMaxGMLevel();
response.previousLevel = entity->GetGMLevel();
response.newLevel = level;
response.Send(entity->GetSystemAddress());
GameMessages::SendChatModeUpdate(entity->GetObjectID(), level);
entity->SetGMLevel(level);
LOG("User %s (%i) has changed their GM level to %i for charID %llu", user->GetUsername().c_str(), user->GetAccountID(), level, entity->GetObjectID());
@@ -88,7 +93,12 @@ namespace DEVGMCommands {
#ifndef DEVELOPER_SERVER
if ((entity->GetGMLevel() > user->GetMaxGMLevel()) || (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN && user->GetMaxGMLevel() == eGameMasterLevel::JUNIOR_DEVELOPER)) {
WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), true, user->GetMaxGMLevel(), entity->GetGMLevel(), eGameMasterLevel::CIVILIAN);
ClientPackets::MakeGMResponse response;
response.success = true;
response.highestLevel = user->GetMaxGMLevel();
response.previousLevel = entity->GetGMLevel();
response.newLevel = eGameMasterLevel::CIVILIAN;
response.Send(entity->GetSystemAddress());
GameMessages::SendChatModeUpdate(entity->GetObjectID(), eGameMasterLevel::CIVILIAN);
entity->SetGMLevel(eGameMasterLevel::CIVILIAN);
@@ -1138,7 +1148,11 @@ namespace DEVGMCommands {
entity->GetCharacter()->SaveXMLToDatabase();
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
return;
});
} else {

View File

@@ -10,7 +10,7 @@
#include "PlayerManager.h"
#include "SlashCommandHandler.h"
#include "VanityUtilities.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "ZoneInstanceManager.h"
#include "Database.h"
@@ -179,7 +179,11 @@ namespace GMZeroCommands {
entity->GetCharacter()->SaveXMLToDatabase();
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
});
}
@@ -210,7 +214,11 @@ namespace GMZeroCommands {
entity->GetCharacter()->SaveXMLToDatabase();
WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(serverIP);
transfer.serverPort = serverPort;
transfer.mythranShift = mythranShift;
transfer.Send(sysAddr);
});
}

View File

@@ -21,7 +21,7 @@
#include "PlayerManager.h"
#include "ServiceType.h"
#include "TradingManager.h"
#include "WorldPackets.h"
#include "ClientPackets.h"
#include "dServer.h"
#include "eServerDisconnectIdentifiers.h"
@@ -158,7 +158,11 @@ namespace {
// player is adopted by it. Same ordered channel as the transfer, so these arrive before it.
if (order.seamless) Game::entityManager->DestructAllEntities(sysAddr);
WorldPackets::SendTransferToWorld(sysAddr, order.targetIp, order.targetPort, order.mythranShift);
ClientPackets::TransferToWorld transfer;
transfer.serverIP = LUString(order.targetIp);
transfer.serverPort = order.targetPort;
transfer.mythranShift = order.mythranShift;
transfer.Send(sysAddr);
LOG("Migration %u: sent %s (%llu) to instance %u", order.migrationId, character->GetName().c_str(), playerId, order.targetInstance);
return true;
}

View File

@@ -150,3 +150,15 @@ void ChatPackets::SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID,
msg.WritePacket(bitStream);
Game::server->Send(bitStream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS);
}
void ChatPackets::RoutedFromClient::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(senderID);
for (const auto byte : data) bitStream.Write(byte);
}
bool ChatPackets::RoutedFromClient::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(senderID));
data.resize(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()));
if (!data.empty()) VALIDATE_READ(bitStream.ReadBits(data.data(), BYTES_TO_BITS(data.size()), true));
return true;
}

View File

@@ -9,6 +9,7 @@
struct SystemAddress;
#include <string>
#include <vector>
#include "dCommonVars.h"
#include "MessageType/Chat.h"
#include "BitStreamUtils.h"
@@ -57,6 +58,17 @@ namespace ChatPackets {
// No Deserialize needed on our end
};
// World -> chat. A chat packet the client sent through the world server (WorldPackets::RoutePacket): its id,
// the object ID of the player who sent it (the chat server has no other way to know), then the routed bytes.
struct RoutedFromClient : public LUBitStream {
LWOOBJID senderID{};
std::vector<uint8_t> data{};
RoutedFromClient(uint8_t routedMessageID = 0) : LUBitStream(ServiceType::CHAT, routedMessageID) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message);
void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, bool broadcast = false);
void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false);

View File

@@ -5,7 +5,15 @@
#include "ClientPackets.h"
#include "dCommonVars.h"
#include "PositionUpdate.h"
#include "eCharacterCreationResponse.h"
#include "eGameMasterLevel.h"
#include "eRenameResponse.h"
#include "LDFFormat.h"
#include "ZCompression.h"
#include <cassert>
#include <memory>
#include <ranges>
#include "eLoginResponse.h"
namespace ClientPackets {
@@ -61,122 +69,374 @@ namespace ClientPackets {
VALIDATE_READ(stamps.Deserialize(bitStream));
return true;
}
}
ChatMessage ClientPackets::HandleChatMessage(Packet* packet) {
CINSTREAM_SKIP_HEADER;
ChatMessage message;
int32_t messageLength{};
inStream.Read(message.chatChannel);
inStream.Read(message.unknown);
inStream.Read(messageLength);
if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message;
for (int32_t i = 0; i < (messageLength - 1); ++i) {
char16_t character;
inStream.Read(character);
message.message.push_back(character);
void LoadStaticZone::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(mapID);
bitStream.Write(instanceID);
bitStream.Write(cloneID);
bitStream.Write(mapChecksum);
bitStream.Write(editorEnabled);
bitStream.Write(editorLevel);
bitStream.Write(playerPosition.x);
bitStream.Write(playerPosition.y);
bitStream.Write(playerPosition.z);
bitStream.Write(instanceType);
}
return message;
}
PositionUpdate ClientPackets::HandleClientPositionUpdate(Packet* packet) {
PositionUpdate update;
CINSTREAM_SKIP_HEADER;
inStream.Read(update.position.x);
inStream.Read(update.position.y);
inStream.Read(update.position.z);
inStream.Read(update.rotation.x);
inStream.Read(update.rotation.y);
inStream.Read(update.rotation.z);
inStream.Read(update.rotation.w);
inStream.Read(update.onGround);
inStream.Read(update.onRail);
bool velocityFlag = false;
inStream.Read(velocityFlag);
if (velocityFlag) {
inStream.Read(update.velocity.x);
inStream.Read(update.velocity.y);
inStream.Read(update.velocity.z);
bool LoadStaticZone::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(mapID));
VALIDATE_READ(bitStream.Read(instanceID));
VALIDATE_READ(bitStream.Read(cloneID));
VALIDATE_READ(bitStream.Read(mapChecksum));
VALIDATE_READ(bitStream.Read(editorEnabled));
VALIDATE_READ(bitStream.Read(editorLevel));
VALIDATE_READ(bitStream.Read(playerPosition.x));
VALIDATE_READ(bitStream.Read(playerPosition.y));
VALIDATE_READ(bitStream.Read(playerPosition.z));
VALIDATE_READ(bitStream.Read(instanceType));
return true;
}
bool angVelocityFlag = false;
inStream.Read(angVelocityFlag);
if (angVelocityFlag) {
inStream.Read(update.angularVelocity.x);
inStream.Read(update.angularVelocity.y);
inStream.Read(update.angularVelocity.z);
}
void CharacterListResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write<uint8_t>(characters.size());
bitStream.Write(selectedCharacterIndex);
// TODO figure out how to use these. Ignoring for now, but reading in if they exist.
bool hasLocalSpaceInfo{};
if (inStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) {
inStream.Read(update.localSpaceInfo.objectId);
inStream.Read(update.localSpaceInfo.position.x);
inStream.Read(update.localSpaceInfo.position.y);
inStream.Read(update.localSpaceInfo.position.z);
bool hasLinearVelocity = false;
if (inStream.Read(hasLinearVelocity) && hasLinearVelocity) {
inStream.Read(update.localSpaceInfo.linearVelocity.x);
inStream.Read(update.localSpaceInfo.linearVelocity.y);
inStream.Read(update.localSpaceInfo.linearVelocity.z);
for (const auto& character : characters) {
bitStream.Write(character.objectID);
bitStream.Write(character.unknown1);
bitStream.Write(character.name);
bitStream.Write(character.unapprovedName);
bitStream.Write<uint8_t>(character.nameRejected);
bitStream.Write<uint8_t>(character.isFreeToPlay);
bitStream.Write(character.unknown2);
bitStream.Write(character.shirtColor);
bitStream.Write(character.shirtStyle);
bitStream.Write(character.pantsColor);
bitStream.Write(character.hairStyle);
bitStream.Write(character.hairColor);
bitStream.Write(character.leftHand);
bitStream.Write(character.rightHand);
bitStream.Write(character.eyebrows);
bitStream.Write(character.eyes);
bitStream.Write(character.mouth);
bitStream.Write(character.unknown3);
bitStream.Write(character.zoneID);
bitStream.Write(character.zoneInstance);
bitStream.Write(character.zoneClone);
bitStream.Write(character.lastLogin);
bitStream.Write<uint16_t>(character.equippedItems.size());
for (const auto item : character.equippedItems) bitStream.Write(item);
}
}
bool hasRemoteInputInfo{};
if (inStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) {
inStream.Read(update.remoteInputInfo.m_RemoteInputX);
inStream.Read(update.remoteInputInfo.m_RemoteInputY);
inStream.Read(update.remoteInputInfo.m_IsPowersliding);
inStream.Read(update.remoteInputInfo.m_IsModified);
bool CharacterListResponse::Deserialize(RakNet::BitStream& bitStream) {
uint8_t count{};
VALIDATE_READ(bitStream.Read(count));
VALIDATE_READ(bitStream.Read(selectedCharacterIndex));
characters.resize(count);
for (auto& character : characters) {
VALIDATE_READ(bitStream.Read(character.objectID));
VALIDATE_READ(bitStream.Read(character.unknown1));
VALIDATE_READ(bitStream.Read(character.name));
VALIDATE_READ(bitStream.Read(character.unapprovedName));
uint8_t flag{};
VALIDATE_READ(bitStream.Read(flag));
character.nameRejected = flag != 0;
VALIDATE_READ(bitStream.Read(flag));
character.isFreeToPlay = flag != 0;
VALIDATE_READ(bitStream.Read(character.unknown2));
VALIDATE_READ(bitStream.Read(character.shirtColor));
VALIDATE_READ(bitStream.Read(character.shirtStyle));
VALIDATE_READ(bitStream.Read(character.pantsColor));
VALIDATE_READ(bitStream.Read(character.hairStyle));
VALIDATE_READ(bitStream.Read(character.hairColor));
VALIDATE_READ(bitStream.Read(character.leftHand));
VALIDATE_READ(bitStream.Read(character.rightHand));
VALIDATE_READ(bitStream.Read(character.eyebrows));
VALIDATE_READ(bitStream.Read(character.eyes));
VALIDATE_READ(bitStream.Read(character.mouth));
VALIDATE_READ(bitStream.Read(character.unknown3));
VALIDATE_READ(bitStream.Read(character.zoneID));
VALIDATE_READ(bitStream.Read(character.zoneInstance));
VALIDATE_READ(bitStream.Read(character.zoneClone));
VALIDATE_READ(bitStream.Read(character.lastLogin));
uint16_t itemCount{};
VALIDATE_READ(bitStream.Read(itemCount));
if (itemCount > BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) / sizeof(LOT)) return false;
character.equippedItems.resize(itemCount);
for (auto& item : character.equippedItems) VALIDATE_READ(bitStream.Read(item));
}
return true;
}
return update;
}
ChatModerationRequest ClientPackets::HandleChatModerationRequest(Packet* packet) {
CINSTREAM_SKIP_HEADER;
ChatModerationRequest request;
inStream.Read(request.chatLevel);
inStream.Read(request.requestID);
for (uint32_t i = 0; i < 42; ++i) {
uint16_t character;
inStream.Read(character);
request.receiver.push_back(static_cast<uint8_t>(character));
void CharacterCreateResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(response);
}
if (!request.receiver.empty()) {
if (std::string(request.receiver.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are
request.receiver = std::string(request.receiver.c_str() + 4, request.receiver.size() - 4);
bool CharacterCreateResponse::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(response));
return true;
}
void CharacterRenameResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(response);
}
bool CharacterRenameResponse::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(response));
return true;
}
void DeleteCharacterResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write<uint8_t>(success);
}
bool DeleteCharacterResponse::Deserialize(RakNet::BitStream& bitStream) {
uint8_t flag{};
VALIDATE_READ(bitStream.Read(flag));
success = flag != 0;
return true;
}
void TransferToWorld::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(serverIP);
bitStream.Write(serverPort);
bitStream.Write<uint8_t>(mythranShift);
}
bool TransferToWorld::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(serverIP));
VALIDATE_READ(bitStream.Read(serverPort));
uint8_t flag{};
VALIDATE_READ(bitStream.Read(flag));
mythranShift = flag != 0;
return true;
}
void ServerStates::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(serverState);
}
bool ServerStates::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(serverState));
return true;
}
void CreateCharacter::Serialize(RakNet::BitStream& bitStream) const {
using namespace std;
RakNet::BitStream data;
LwoNameValue ldfData;
ldfData.Insert<LWOOBJID>(u"objid", objectID);
ldfData.Insert<LOT>(u"template", templateID);
ldfData.Insert<string>(u"xmlData", xmlData);
ldfData.Insert<u16string>(u"name", name);
ldfData.Insert<int32_t>(u"gmlevel", static_cast<int32_t>(gmLevel));
ldfData.Insert<int32_t>(u"chatmode", chatMode);
ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", propertyCloneID);
data.Write<uint32_t>(ldfData.values.size());
for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data);
//Compress the data before sending:
const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed());
auto compressedData = std::make_unique<uint8_t[]>(reservedSize);
size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize);
assert(size <= reservedSize);
bitStream.Write<uint32_t>(size + 9); //size of data + header bytes (8)
bitStream.Write<uint8_t>(1); //compressed boolean, true
bitStream.Write<uint32_t>(data.GetNumberOfBytesUsed());
bitStream.Write<uint32_t>(size);
/**
* In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap
* and then compress the data. In the off chance that the compression actually increases the size,
* an assertion is done to prevent bad data from being saved or sent.
*/
#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'.
bitStream.WriteAlignedBytes(compressedData.get(), size);
#pragma warning(default:6385)
}
namespace {
bool ReadLdfKey(RakNet::BitStream& bitStream, std::u16string& key) {
uint8_t keyBytes{};
VALIDATE_READ(bitStream.Read(keyBytes));
key.resize(keyBytes / sizeof(char16_t));
for (auto& character : key) VALIDATE_READ(bitStream.Read(character));
return true;
}
}
uint16_t messageLength;
inStream.Read(messageLength);
if (messageLength > MAX_MESSAGE_LENGTH) return request;
for (uint32_t i = 0; i < messageLength; ++i) {
uint16_t character;
inStream.Read(character);
request.message.push_back(static_cast<uint8_t>(character));
bool CreateCharacter::Deserialize(RakNet::BitStream& bitStream) {
uint32_t sizePlusHeader{};
uint8_t compressed{};
uint32_t uncompressedSize{};
uint32_t compressedSize{};
VALIDATE_READ(bitStream.Read(sizePlusHeader));
VALIDATE_READ(bitStream.Read(compressed));
VALIDATE_READ(bitStream.Read(uncompressedSize));
VALIDATE_READ(bitStream.Read(compressedSize));
if (compressed != 1 || sizePlusHeader != compressedSize + 9) return false;
if (compressedSize > BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) || uncompressedSize > MAX_MESSAGE_LENGTH) return false;
std::vector<uint8_t> compressedData(compressedSize);
if (compressedSize > 0) VALIDATE_READ(bitStream.ReadAlignedBytes(compressedData.data(), compressedSize));
std::vector<uint8_t> uncompressed(uncompressedSize);
int32_t error{};
const auto decompressedSize = ZCompression::Decompress(compressedData.data(), compressedSize, uncompressed.data(), uncompressedSize, error);
if (decompressedSize != static_cast<int32_t>(uncompressedSize)) return false;
RakNet::BitStream data(uncompressed.data(), uncompressed.size(), false);
uint32_t count{};
VALIDATE_READ(data.Read(count));
for (uint32_t i = 0; i < count; i++) {
std::u16string key;
VALIDATE_READ(ReadLdfKey(data, key));
uint8_t type{};
VALIDATE_READ(data.Read(type));
switch (type) {
case LDF_TYPE_S32: {
int32_t value{};
VALIDATE_READ(data.Read(value));
if (key == u"template") templateID = value;
else if (key == u"gmlevel") gmLevel = static_cast<eGameMasterLevel>(value);
else if (key == u"chatmode") chatMode = value;
else if (key == u"propertycloneid") propertyCloneID = value;
break;
}
case LDF_TYPE_OBJID: {
int64_t value{};
VALIDATE_READ(data.Read(value));
if (key == u"objid") objectID = value;
else if (key == u"reputation") reputation = value;
break;
}
case LDF_TYPE_UTF_16: {
std::u16string value;
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(data, value));
if (key == u"name") name = value;
break;
}
case LDF_TYPE_UTF_8: {
std::string value;
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(data, value));
if (key == u"xmlData") xmlData = value;
break;
}
default:
return false; // CreateCharacter never uses other types
}
}
return true;
}
return request;
}
void ChatModerationString::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write<uint8_t>(requestAccepted); // Is sentence ok?
bitStream.Write(sourceID); // Source ID, unknown
int32_t ClientPackets::SendTop5HelpIssues(Packet* packet) {
CINSTREAM_SKIP_HEADER;
int32_t language = 0;
inStream.Read(language);
return language;
bitStream.Write(requestID); // request ID
bitStream.Write(chatMode); // chat mode
bitStream.Write(receiver); // receiver name
for (const auto& [start, length] : rejectedSegments) {
bitStream.Write(start); // start index
bitStream.Write(length); // length
}
for (auto i = rejectedSegments.size(); i < 64; i++) {
bitStream.Write<uint16_t>(0);
}
}
bool ChatModerationString::Deserialize(RakNet::BitStream& bitStream) {
uint8_t accepted{};
VALIDATE_READ(bitStream.Read(accepted));
requestAccepted = accepted != 0;
VALIDATE_READ(bitStream.Read(sourceID));
VALIDATE_READ(bitStream.Read(requestID));
VALIDATE_READ(bitStream.Read(chatMode));
VALIDATE_READ(bitStream.Read(receiver));
rejectedSegments.clear();
for (int i = 0; i < 64; i++) {
uint8_t start{};
uint8_t length{};
VALIDATE_READ(bitStream.Read(start));
VALIDATE_READ(bitStream.Read(length));
if (start != 0 || length != 0) rejectedSegments.emplace(start, length);
}
return true;
}
void MakeGMResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write<uint8_t>(success);
bitStream.Write(static_cast<uint16_t>(highestLevel));
bitStream.Write(static_cast<uint16_t>(previousLevel));
bitStream.Write(static_cast<uint16_t>(newLevel));
}
bool MakeGMResponse::Deserialize(RakNet::BitStream& bitStream) {
uint8_t flag{};
uint16_t highest{};
uint16_t previous{};
uint16_t next{};
VALIDATE_READ(bitStream.Read(flag));
VALIDATE_READ(bitStream.Read(highest));
VALIDATE_READ(bitStream.Read(previous));
VALIDATE_READ(bitStream.Read(next));
success = flag != 0;
highestLevel = static_cast<eGameMasterLevel>(highest);
previousLevel = static_cast<eGameMasterLevel>(previous);
newLevel = static_cast<eGameMasterLevel>(next);
return true;
}
void HTTPMonitorInfoResponse::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(port);
bitStream.Write<uint8_t>(openWeb);
bitStream.Write<uint8_t>(supportsSum);
bitStream.Write<uint8_t>(supportsDetail);
bitStream.Write<uint8_t>(supportsWho);
bitStream.Write<uint8_t>(supportsObjects);
}
bool HTTPMonitorInfoResponse::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(port));
for (auto* flag : { &openWeb, &supportsSum, &supportsDetail, &supportsWho, &supportsObjects }) {
uint8_t value{};
VALIDATE_READ(bitStream.Read(value));
*flag = value != 0;
}
return true;
}
void DebugOutput::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, data);
}
bool DebugOutput::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, data));
return true;
}
}

View File

@@ -8,19 +8,23 @@
#include <cstdint>
#include <ctime>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "BitStreamUtils.h"
#include "MessageType/Client.h"
#include "Stamps.h"
#include "dCommonVars.h"
#include "NiPoint3.h"
enum class eCharacterCreationResponse : uint8_t;
enum class eGameMasterLevel : uint8_t;
enum class eRenameResponse : uint8_t;
enum class eLoginResponse : uint8_t;
class PositionUpdate;
struct Packet;
enum class Language : uint32_t {
en_US,
pl_US,
@@ -28,19 +32,6 @@ enum class Language : uint32_t {
en_GB,
};
struct ChatMessage {
uint8_t chatChannel = 0;
uint16_t unknown = 0;
std::u16string message;
};
struct ChatModerationRequest {
uint8_t chatLevel = 0;
uint8_t requestID = 0;
std::string receiver;
std::string message;
};
namespace ClientPackets {
// Server -> client. The auth server's answer to a login (the client's clientLoginResponsePacket).
struct LoginResponse : public LUBitStream {
@@ -73,10 +64,173 @@ namespace ClientPackets {
bool Deserialize(RakNet::BitStream& bitStream) override;
};
ChatMessage HandleChatMessage(Packet* packet);
PositionUpdate HandleClientPositionUpdate(Packet* packet);
ChatModerationRequest HandleChatModerationRequest(Packet* packet);
int32_t SendTop5HelpIssues(Packet* packet);
// Server -> client. Tells the client which zone to load (ClientLoadStaticZonePacket in the client).
struct LoadStaticZone : public LUBitStream {
uint16_t mapID{};
uint16_t instanceID{};
uint32_t cloneID{}; // DLU has always sent 0 here
uint32_t mapChecksum{};
uint8_t editorEnabled{};
uint8_t editorLevel{};
NiPoint3 playerPosition{};
uint32_t instanceType{}; // Change this to eventually use 4 on activity worlds
LoadStaticZone() : LUBitStream(ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. The account's characters for the character select screen.
struct CharacterListResponse : public LUBitStream {
struct Character {
LWOOBJID objectID{};
uint32_t unknown1{};
LUWString name{ 33 };
LUWString unapprovedName{ 33 };
bool nameRejected{}; // Written as one byte
bool isFreeToPlay{}; // Written as one byte
LUString unknown2{ 10 };
uint32_t shirtColor{};
uint32_t shirtStyle{};
uint32_t pantsColor{};
uint32_t hairStyle{};
uint32_t hairColor{};
uint32_t leftHand{};
uint32_t rightHand{};
uint32_t eyebrows{};
uint32_t eyes{};
uint32_t mouth{};
uint32_t unknown3{};
uint16_t zoneID{};
uint16_t zoneInstance{};
uint32_t zoneClone{};
uint64_t lastLogin{};
std::vector<LOT> equippedItems{}; // After a u16 count
};
// Written as a u8 count first
uint8_t selectedCharacterIndex{}; // TODO: Pick the most recent played index. DLU always sends 0
std::vector<Character> characters{};
CharacterListResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client.
struct CharacterCreateResponse : public LUBitStream {
eCharacterCreationResponse response{};
CharacterCreateResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client.
struct CharacterRenameResponse : public LUBitStream {
eRenameResponse response{};
CharacterRenameResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client.
struct DeleteCharacterResponse : public LUBitStream {
bool success{}; // Written as one byte
DeleteCharacterResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. Sends the client to another world server.
struct TransferToWorld : public LUBitStream {
LUString serverIP{ 33 };
uint16_t serverPort{};
bool mythranShift{}; // Written as one byte
TransferToWorld() : LUBitStream(ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client.
struct ServerStates : public LUBitStream {
uint8_t serverState = 1; // If the server is receiving this request, it probably is ready anyway.
ServerStates() : LUBitStream(ServiceType::CLIENT, MessageType::Client::SERVER_STATES) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. The player's own character: a compressed LDF list.
// On the wire: u32 compressed size + 9, u8 1 (compressed), u32 uncompressed size, u32 compressed size, the zlib data.
struct CreateCharacter : public LUBitStream {
LWOOBJID objectID{};
LOT templateID = 1;
std::string xmlData{};
std::u16string name{};
eGameMasterLevel gmLevel{};
int32_t chatMode{}; // DLU sends the GM level
int64_t reputation{};
int32_t propertyCloneID{};
CreateCharacter() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. The answer to WorldPackets::StringCheck.
struct ChatModerationString : public LUBitStream {
bool requestAccepted{}; // Written as one byte
uint16_t sourceID = 0x16; // Unknown
uint8_t requestID{};
uint8_t chatMode{};
LUWString receiver{ 42 };
// (start, length) of each part that was not accepted, in 64 two byte slots; unused slots are 0
std::set<std::pair<uint8_t, uint8_t>> rejectedSegments{};
ChatModerationString() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client.
struct MakeGMResponse : public LUBitStream {
bool success{}; // Written as one byte
eGameMasterLevel highestLevel{}; // Written as u16
eGameMasterLevel previousLevel{}; // Written as u16
eGameMasterLevel newLevel{}; // Written as u16
MakeGMResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. DLU does not send it.
struct HTTPMonitorInfoResponse : public LUBitStream {
uint16_t port = 80;
// Each written as one byte
bool openWeb = false;
bool supportsSum = false;
bool supportsDetail = false;
bool supportsWho = false;
bool supportsObjects = false;
HTTPMonitorInfoResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Server -> client. Text for the client's debug output. DLU does not send it.
struct DebugOutput : public LUBitStream {
std::string data{}; // After a u32 length
DebugOutput() : LUBitStream(ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
};
#endif // CLIENTPACKETS_H

View File

@@ -1,180 +1,351 @@
#include "WorldPackets.h"
#include "dCommonVars.h"
#include "BitStream.h"
#include "GeneralUtils.h"
#include "Logger.h"
#include "Game.h"
#include "LDFFormat.h"
#include "dServer.h"
#include "ZCompression.h"
#include "ServiceType.h"
#include "BitStreamUtils.h"
#include <iostream>
#include <ranges>
#include <algorithm>
void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(port);
bitStream.Write<uint8_t>(openWeb);
bitStream.Write<uint8_t>(supportsSum);
bitStream.Write<uint8_t>(supportsDetail);
bitStream.Write<uint8_t>(supportsWho);
bitStream.Write<uint8_t>(supportsObjects);
}
void WorldPackets::SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE);
bitStream.Write<uint16_t>(zone.GetMapID());
bitStream.Write<uint16_t>(zone.GetInstanceID());
//bitStream.Write<uint32_t>(zone.GetCloneID());
bitStream.Write(0);
bitStream.Write(checksum);
bitStream.Write<uint16_t>(0); // ??
bitStream.Write(x);
bitStream.Write(y);
bitStream.Write(z);
bitStream.Write<uint32_t>(0); // Change this to eventually use 4 on activity worlds
SEND_PACKET;
}
void WorldPackets::SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE);
bitStream.Write(response);
SEND_PACKET;
}
void WorldPackets::SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE);
bitStream.Write(response);
SEND_PACKET;
}
void WorldPackets::SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE);
bitStream.Write<uint8_t>(response);
SEND_PACKET;
}
void WorldPackets::SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD);
bitStream.Write(LUString(serverIP));
bitStream.Write<uint16_t>(serverPort);
bitStream.Write<uint8_t>(mythranShift);
SEND_PACKET;
}
void WorldPackets::SendServerState(const SystemAddress& sysAddr) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SERVER_STATES);
bitStream.Write<uint8_t>(1); //If the server is receiving this request, it probably is ready anyway.
SEND_PACKET;
}
void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID) {
using namespace std;
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER);
RakNet::BitStream data;
LwoNameValue ldfData;
ldfData.Insert<LWOOBJID>(u"objid", player);
ldfData.Insert<LOT>(u"template", 1);
ldfData.Insert<string>(u"xmlData", xmlData);
ldfData.Insert<u16string>(u"name", username);
ldfData.Insert<int32_t>(u"gmlevel", static_cast<int32_t>(gm));
ldfData.Insert<int32_t>(u"chatmode", static_cast<int32_t>(gm));
ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", cloneID);
data.Write<uint32_t>(ldfData.values.size());
for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data);
//Compress the data before sending:
const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed());
auto compressedData = std::make_unique<uint8_t[]>(reservedSize);
size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize);
assert(size <= reservedSize);
bitStream.Write<uint32_t>(size + 9); //size of data + header bytes (8)
bitStream.Write<uint8_t>(1); //compressed boolean, true
bitStream.Write<uint32_t>(data.GetNumberOfBytesUsed());
bitStream.Write<uint32_t>(size);
/**
* In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap
* and then compress the data. In the off chance that the compression actually increases the size,
* an assertion is done to prevent bad data from being saved or sent.
*/
#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'.
bitStream.WriteAlignedBytes(compressedData.get(), size);
#pragma warning(default:6385)
SEND_PACKET;
LOG("Sent CreateCharacter for ID: %llu", player);
}
void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING);
bitStream.Write<uint8_t>(unacceptedItems.empty()); // Is sentence ok?
bitStream.Write<uint16_t>(0x16); // Source ID, unknown
bitStream.Write<uint8_t>(requestID); // request ID
bitStream.Write<char>(0); // chat mode
bitStream.Write(LUWString(receiver, 42)); // receiver name
for (auto it : unacceptedItems) {
bitStream.Write<uint8_t>(it.first); // start index
bitStream.Write<uint8_t>(it.second); // length
namespace {
// Copies every unread bit of from into to.
bool ReadRest(RakNet::BitStream& from, RakNet::BitStream& to) {
to.Reset();
return from.Read(to, from.GetNumberOfUnreadBits());
}
for (int i = unacceptedItems.size(); 64 > i; i++) {
bitStream.Write<uint16_t>(0);
void WriteAll(RakNet::BitStream& to, const RakNet::BitStream& from) {
auto& nonConst = const_cast<RakNet::BitStream&>(from); // GetData is not const in RakNet
to.WriteBits(nonConst.GetData(), nonConst.GetNumberOfBitsUsed(), false);
}
}
namespace WorldPackets {
void Validation::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(username);
bitStream.Write(sessionKey);
bitStream.Write(fdbChecksum);
}
SEND_PACKET;
}
void WorldPackets::SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE);
bitStream.Write<uint8_t>(success);
bitStream.Write(static_cast<uint16_t>(highestLevel));
bitStream.Write(static_cast<uint16_t>(prevLevel));
bitStream.Write(static_cast<uint16_t>(newLevel));
SEND_PACKET;
}
void WorldPackets::SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE);
info.Serialize(bitStream);
SEND_PACKET;
}
void WorldPackets::SendDebugOuput(const SystemAddress& sysAddr, const std::string& data) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT);
bitStream.Write<uint32_t>(data.size());
bitStream.Write(data);
SEND_PACKET;
bool Validation::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(username));
// sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird
VALIDATE_READ(bitStream.Read(sessionKey));
VALIDATE_READ(bitStream.Read(fdbChecksum));
return true;
}
void CharacterCreateRequest::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(name);
bitStream.Write(firstNameIndex);
bitStream.Write(middleNameIndex);
bitStream.Write(lastNameIndex);
for (const auto byte : unknown) bitStream.Write(byte);
bitStream.Write(shirtColor);
bitStream.Write(shirtStyle);
bitStream.Write(pantsColor);
bitStream.Write(hairStyle);
bitStream.Write(hairColor);
bitStream.Write(leftHand);
bitStream.Write(rightHand);
bitStream.Write(eyebrows);
bitStream.Write(eyes);
bitStream.Write(mouth);
}
bool CharacterCreateRequest::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(name));
VALIDATE_READ(bitStream.Read(firstNameIndex));
VALIDATE_READ(bitStream.Read(middleNameIndex));
VALIDATE_READ(bitStream.Read(lastNameIndex));
for (auto& byte : unknown) VALIDATE_READ(bitStream.Read(byte));
VALIDATE_READ(bitStream.Read(shirtColor));
VALIDATE_READ(bitStream.Read(shirtStyle));
VALIDATE_READ(bitStream.Read(pantsColor));
VALIDATE_READ(bitStream.Read(hairStyle));
VALIDATE_READ(bitStream.Read(hairColor));
VALIDATE_READ(bitStream.Read(leftHand));
VALIDATE_READ(bitStream.Read(rightHand));
VALIDATE_READ(bitStream.Read(eyebrows));
VALIDATE_READ(bitStream.Read(eyes));
VALIDATE_READ(bitStream.Read(mouth));
return true;
}
void CharacterLoginRequest::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(playerID);
}
bool CharacterLoginRequest::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(playerID));
return true;
}
void GameMessage::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(objectID);
bitStream.Write(messageID);
WriteAll(bitStream, data);
}
bool GameMessage::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(objectID));
VALIDATE_READ(bitStream.Read(messageID));
VALIDATE_READ(ReadRest(bitStream, data));
return true;
}
void CharacterDeleteRequest::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(objectID);
}
bool CharacterDeleteRequest::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(objectID));
return true;
}
void CharacterRenameRequest::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(objectID);
bitStream.Write(name);
}
bool CharacterRenameRequest::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(objectID));
VALIDATE_READ(bitStream.Read(name));
return true;
}
void LevelLoadComplete::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(mapID);
bitStream.Write(instanceID);
bitStream.Write(cloneID);
}
bool LevelLoadComplete::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(mapID));
VALIDATE_READ(bitStream.Read(instanceID));
VALIDATE_READ(bitStream.Read(cloneID));
return true;
}
void PositionUpdate::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(update.position.x);
bitStream.Write(update.position.y);
bitStream.Write(update.position.z);
bitStream.Write(update.rotation.x);
bitStream.Write(update.rotation.y);
bitStream.Write(update.rotation.z);
bitStream.Write(update.rotation.w);
bitStream.Write(update.onGround);
bitStream.Write(update.onRail);
bitStream.Write(hasVelocity);
if (hasVelocity) {
bitStream.Write(update.velocity.x);
bitStream.Write(update.velocity.y);
bitStream.Write(update.velocity.z);
}
bitStream.Write(hasAngularVelocity);
if (hasAngularVelocity) {
bitStream.Write(update.angularVelocity.x);
bitStream.Write(update.angularVelocity.y);
bitStream.Write(update.angularVelocity.z);
}
bitStream.Write(hasLocalSpaceInfo);
if (hasLocalSpaceInfo) {
bitStream.Write(update.localSpaceInfo.objectId);
bitStream.Write(update.localSpaceInfo.position.x);
bitStream.Write(update.localSpaceInfo.position.y);
bitStream.Write(update.localSpaceInfo.position.z);
bitStream.Write(hasLinearVelocity);
if (hasLinearVelocity) {
bitStream.Write(update.localSpaceInfo.linearVelocity.x);
bitStream.Write(update.localSpaceInfo.linearVelocity.y);
bitStream.Write(update.localSpaceInfo.linearVelocity.z);
}
}
bitStream.Write(hasRemoteInputInfo);
if (hasRemoteInputInfo) {
bitStream.Write(update.remoteInputInfo.m_RemoteInputX);
bitStream.Write(update.remoteInputInfo.m_RemoteInputY);
bitStream.Write(update.remoteInputInfo.m_IsPowersliding);
bitStream.Write(update.remoteInputInfo.m_IsModified);
}
}
bool PositionUpdate::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(update.position.x));
VALIDATE_READ(bitStream.Read(update.position.y));
VALIDATE_READ(bitStream.Read(update.position.z));
VALIDATE_READ(bitStream.Read(update.rotation.x));
VALIDATE_READ(bitStream.Read(update.rotation.y));
VALIDATE_READ(bitStream.Read(update.rotation.z));
VALIDATE_READ(bitStream.Read(update.rotation.w));
VALIDATE_READ(bitStream.Read(update.onGround));
VALIDATE_READ(bitStream.Read(update.onRail));
VALIDATE_READ(bitStream.Read(hasVelocity));
if (hasVelocity) {
VALIDATE_READ(bitStream.Read(update.velocity.x));
VALIDATE_READ(bitStream.Read(update.velocity.y));
VALIDATE_READ(bitStream.Read(update.velocity.z));
}
VALIDATE_READ(bitStream.Read(hasAngularVelocity));
if (hasAngularVelocity) {
VALIDATE_READ(bitStream.Read(update.angularVelocity.x));
VALIDATE_READ(bitStream.Read(update.angularVelocity.y));
VALIDATE_READ(bitStream.Read(update.angularVelocity.z));
}
// The last two sections may be missing entirely (DLU has always accepted that)
hasLocalSpaceInfo = false;
hasLinearVelocity = false;
if (bitStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) {
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.objectId));
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.x));
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.y));
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.z));
if (bitStream.Read(hasLinearVelocity) && hasLinearVelocity) {
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.x));
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.y));
VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.z));
}
}
hasRemoteInputInfo = false;
if (bitStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) {
VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_RemoteInputX));
VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_RemoteInputY));
VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_IsPowersliding));
VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_IsModified));
}
return true;
}
void MailPacket::Serialize(RakNet::BitStream& bitStream) const {
WriteAll(bitStream, data);
}
bool MailPacket::Deserialize(RakNet::BitStream& bitStream) {
return ReadRest(bitStream, data);
}
void RoutePacket::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(size);
bitStream.Write(routedService);
bitStream.Write(routedMessageID);
bitStream.Write(padding);
for (const auto byte : routedData) bitStream.Write(byte);
}
bool RoutePacket::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(size));
VALIDATE_READ(bitStream.Read(routedService));
VALIDATE_READ(bitStream.Read(routedMessageID));
VALIDATE_READ(bitStream.Read(padding));
routedData.resize(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()));
if (!routedData.empty()) VALIDATE_READ(bitStream.ReadBits(routedData.data(), BYTES_TO_BITS(routedData.size()), true));
return true;
}
ChatPackets::RoutedFromClient RoutePacket::ToChat(const LWOOBJID senderID) const {
constexpr size_t ROUTED_DATA_SKIP = 4;
ChatPackets::RoutedFromClient routed(static_cast<uint8_t>(routedMessageID & 0xFF));
routed.senderID = senderID;
for (size_t i = ROUTED_DATA_SKIP; i - ROUTED_DATA_SKIP < size && i < routedData.size(); ++i) {
routed.data.push_back(routedData[i]);
}
return routed;
}
void StringCheck::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(chatLevel);
bitStream.Write(requestID);
auto fixedReceiver = receiver;
fixedReceiver.resize(42);
bitStream.Write(fixedReceiver);
bitStream.Write<uint16_t>(message.size());
bitStream.Write(message);
}
bool StringCheck::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(chatLevel));
VALIDATE_READ(bitStream.Read(requestID));
receiver.resize(42);
VALIDATE_READ(bitStream.ReadBits(reinterpret_cast<unsigned char*>(receiver.data()), BYTES_TO_BITS(receiver.size() * sizeof(char16_t)), true));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint16_t>(bitStream, message));
return true;
}
std::string StringCheck::GetNarrowReceiver() const {
std::string narrow;
for (const auto character : receiver) narrow.push_back(static_cast<uint8_t>(character));
if (!narrow.empty()) {
if (std::string(narrow.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are
narrow = std::string(narrow.c_str() + 4, narrow.size() - 4);
}
}
return narrow;
}
std::string StringCheck::GetNarrowMessage() const {
std::string narrow;
for (const auto character : message) narrow.push_back(static_cast<uint8_t>(character));
return narrow;
}
void GeneralChatMessage::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(chatChannel);
bitStream.Write(unknown);
bitStream.Write<int32_t>(message.size() + 1);
bitStream.Write(message);
bitStream.Write<char16_t>(u'\0');
}
bool GeneralChatMessage::Deserialize(RakNet::BitStream& bitStream) {
int32_t messageLength{};
VALIDATE_READ(bitStream.Read(chatChannel));
VALIDATE_READ(bitStream.Read(unknown));
VALIDATE_READ(bitStream.Read(messageLength));
if (messageLength < 0 || static_cast<uint32_t>(messageLength) > MAX_MESSAGE_LENGTH) return false;
message.clear();
for (int32_t i = 0; i < (messageLength - 1); ++i) {
char16_t character;
VALIDATE_READ(bitStream.Read(character));
message.push_back(character);
}
// The null terminator the count includes; DLU never needed it
if (messageLength > 0) bitStream.IgnoreBits(std::min<BitSize_t>(bitStream.GetNumberOfUnreadBits(), BYTES_TO_BITS(sizeof(char16_t))));
return true;
}
void HandleFunness::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(cheatInfo);
bitStream.Write(cheatType);
}
bool HandleFunness::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(cheatInfo));
VALIDATE_READ(bitStream.Read(cheatType));
return true;
}
void UIHelpTop5::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(language);
}
bool UIHelpTop5::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(language));
return true;
}
}

View File

@@ -1,41 +1,214 @@
#ifndef WORLDPACKETS_H
#define WORLDPACKETS_H
#include "BitStreamUtils.h"
#include "ChatPackets.h"
#include "dCommonVars.h"
#include <vector>
#include "eFunnessTypes.h"
#include "MessageType/Game.h"
#include "MessageType/World.h"
#include "PositionUpdate.h"
#include "RakNetTypes.h"
#include <array>
#include <cstdint>
#include <string>
#include <vector>
class User;
struct SystemAddress;
enum class eGameMasterLevel : uint8_t;
enum class eCharacterCreationResponse : uint8_t;
enum class eRenameResponse : uint8_t;
namespace RakNet {
class BitStream;
};
struct HTTPMonitorInfo {
uint16_t port = 80;
bool openWeb = false;
bool supportsSum = false;
bool supportsDetail = false;
bool supportsWho = false;
bool supportsObjects = false;
void Serialize(RakNet::BitStream &bitstream) const;
};
// Packets of ServiceType::WORLD: what a client sends to a world server. The server's answers are ClientPackets.
// The structs only read and write bytes; the world server attaches the handlers (see WorldServer.cpp).
namespace WorldPackets {
void SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone);
void SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response);
void SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response);
void SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response);
void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift);
void SendServerState(const SystemAddress& sysAddr);
void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID);
void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems);
void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel);
void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info);
void SendDebugOuput(const SystemAddress& sysAddr, const std::string& data);
// Base of every world packet: the dispatcher sets sysAddr before Deserialize and Handle.
struct WorldLUBitStream : public LUBitStream {
SystemAddress sysAddr = UNASSIGNED_SYSTEM_ADDRESS;
WorldLUBitStream() = default;
template<typename T>
WorldLUBitStream(T internalPacketID) : LUBitStream(ServiceType::WORLD, internalPacketID) {}
};
// Client -> server. Who the client is and the session key auth gave it.
struct Validation : public WorldLUBitStream {
LUWString username{ 33 };
LUWString sessionKey{ 33 };
LUString fdbChecksum{ 32 };
// The client may send one more byte after the checksum (a null terminator); it is ignored.
Validation() : WorldLUBitStream(MessageType::World::VALIDATION) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. No payload.
struct CharacterListRequest : public WorldLUBitStream {
CharacterListRequest() : WorldLUBitStream(MessageType::World::CHARACTER_LIST_REQUEST) {}
};
// Client -> server. A new character from the character creator.
struct CharacterCreateRequest : public WorldLUBitStream {
LUWString name{ 33 };
uint32_t firstNameIndex{};
uint32_t middleNameIndex{};
uint32_t lastNameIndex{};
std::array<uint8_t, 9> unknown{}; // Not used by DLU
uint32_t shirtColor{};
uint32_t shirtStyle{};
uint32_t pantsColor{};
uint32_t hairStyle{};
uint32_t hairColor{};
uint32_t leftHand{};
uint32_t rightHand{};
uint32_t eyebrows{};
uint32_t eyes{};
uint32_t mouth{};
// The client sends one more byte; it is ignored.
CharacterCreateRequest() : WorldLUBitStream(MessageType::World::CHARACTER_CREATE_REQUEST) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. The character the player picked to play.
struct CharacterLoginRequest : public WorldLUBitStream {
LWOOBJID playerID{};
CharacterLoginRequest() : WorldLUBitStream(MessageType::World::LOGIN_REQUEST) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. A game message; data holds its parameters for GameMessageHandler.
struct GameMessage : public WorldLUBitStream {
LWOOBJID objectID{};
MessageType::Game messageID{};
RakNet::BitStream data;
GameMessage() : WorldLUBitStream(MessageType::World::GAME_MSG) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server.
struct CharacterDeleteRequest : public WorldLUBitStream {
LWOOBJID objectID{};
CharacterDeleteRequest() : WorldLUBitStream(MessageType::World::CHARACTER_DELETE_REQUEST) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server.
struct CharacterRenameRequest : public WorldLUBitStream {
LWOOBJID objectID{};
LUWString name{ 33 };
CharacterRenameRequest() : WorldLUBitStream(MessageType::World::CHARACTER_RENAME_REQUEST) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. The client finished loading the zone from LoadStaticZone.
struct LevelLoadComplete : public WorldLUBitStream {
// The zone the client loaded; not used by DLU
uint16_t mapID{};
uint16_t instanceID{};
uint32_t cloneID{};
LevelLoadComplete() : WorldLUBitStream(MessageType::World::LEVEL_LOAD_COMPLETE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. Where the player's character is.
struct PositionUpdate : public WorldLUBitStream {
::PositionUpdate update{};
// Which optional sections are present
bool hasVelocity{};
bool hasAngularVelocity{};
bool hasLocalSpaceInfo{};
bool hasLinearVelocity{}; // Inside the local space info
bool hasRemoteInputInfo{};
PositionUpdate() : WorldLUBitStream(MessageType::World::POSITION_UPDATE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. A mail request; data holds it for Mail::HandleMail (which has its own sub-header).
struct MailPacket : public WorldLUBitStream {
RakNet::BitStream data;
MailPacket() : WorldLUBitStream(MessageType::World::MAIL) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. A packet for the chat server, wrapped: its size, then the chat packet's own header
// (service, id, pad) and body.
struct RoutePacket : public WorldLUBitStream {
uint32_t size{};
ServiceType routedService{};
uint32_t routedMessageID{};
uint8_t padding{};
std::vector<uint8_t> routedData{}; // Everything after the routed header
RoutePacket() : WorldLUBitStream(MessageType::World::ROUTE_PACKET) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
// What DLU forwards to the chat server: the first byte of the routed id, the sender, then at most size bytes
// of the routed data from 4 bytes in (the chat server skips 4 bytes after the sender ID it reads).
ChatPackets::RoutedFromClient ToChat(LWOOBJID senderID) const;
};
// Client -> server. Asks whether what the player is typing may be sent.
struct StringCheck : public WorldLUBitStream {
uint8_t chatLevel{};
uint8_t requestID{};
std::u16string receiver = std::u16string(42, u'\0'); // Always 42 characters on the wire
std::u16string message{}; // After a u16 character count
StringCheck() : WorldLUBitStream(MessageType::World::STRING_CHECK) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
// The receiver as DLU has always used it: each character cut to one byte, all 42 of them (the text is
// followed by nulls and whatever else the client left in its buffer), without a leading "[GM]".
std::string GetNarrowReceiver() const;
// The message with each character cut to one byte.
std::string GetNarrowMessage() const;
};
// Client -> server. A message in the zone's chat.
struct GeneralChatMessage : public WorldLUBitStream {
uint8_t chatChannel{};
uint16_t unknown{};
// On the wire: an i32 count that includes a null terminator, the characters, then the terminator
std::u16string message{};
GeneralChatMessage() : WorldLUBitStream(MessageType::World::GENERAL_CHAT_MESSAGE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. The client's anti cheat noticed something.
struct HandleFunness : public WorldLUBitStream {
float cheatInfo{};
eFunnessTypes cheatType{};
HandleFunness() : WorldLUBitStream(MessageType::World::HANDLE_FUNNESS) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// Client -> server. The help screen wants its top 5 issues.
struct UIHelpTop5 : public WorldLUBitStream {
int32_t language{}; // 0: en_US, 1: pl_US, 2: de_DE, 3: en_GB
UIHelpTop5() : WorldLUBitStream(MessageType::World::UI_HELP_TOP_5) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
}
#endif // WORLDPACKETS_H

View File

@@ -12,6 +12,8 @@
#include <ctime>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
#include "MD5.h"
@@ -773,7 +775,14 @@ void HandleMasterPacket(Packet* packet) {
z = pos.z;
}
WorldPackets::SendLoadStaticZone(it->second.sysAddr, x, y, z, zone->GetChecksum(), Game::zoneManager->GetZoneID());
ClientPackets::LoadStaticZone loadZone;
const auto zoneID = Game::zoneManager->GetZoneID();
loadZone.mapID = zoneID.GetMapID();
loadZone.instanceID = zoneID.GetInstanceID();
loadZone.cloneID = 0; // DLU has always sent 0 here, whatever the clone
loadZone.mapChecksum = zone->GetChecksum();
loadZone.playerPosition = NiPoint3(x, y, z);
loadZone.Send(it->second.sysAddr);
}
if (Game::server->GetZoneID() == 0) {
@@ -1029,8 +1038,18 @@ void LoadPlayer(const SystemAddress& sysAddr) {
destroyableComponent->FixStats();
}
WorldPackets::SendCreateCharacter(sysAddr, characterComponent->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel(), c->GetPropertyCloneID());
WorldPackets::SendServerState(sysAddr);
ClientPackets::CreateCharacter createCharacter;
createCharacter.objectID = player->GetObjectID();
createCharacter.xmlData = c->GetXMLData();
createCharacter.name = username;
createCharacter.gmLevel = c->GetGMLevel();
createCharacter.chatMode = static_cast<int32_t>(c->GetGMLevel());
createCharacter.reputation = characterComponent->GetReputation();
createCharacter.propertyCloneID = c->GetPropertyCloneID();
createCharacter.Send(sysAddr);
LOG("Sent CreateCharacter for ID: %llu", player->GetObjectID());
ClientPackets::ServerStates serverStates;
serverStates.Send(sysAddr);
const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID());
@@ -1192,6 +1211,463 @@ void CleanupDisconnectedUser(const SystemAddress& sysAddr) {
Game::server->SendToMaster(bitStream);
}
// The world server's handlers for what clients send (ServiceType::WORLD). WorldPackets has the structs; each one is
// handled by overriding Handle here, where the server's state is.
namespace {
struct ValidationPacket final : public WorldPackets::Validation {
void Handle() override {
const auto& clientDatabaseChecksum = fdbChecksum;
// If the check is turned on, validate the client's database checksum.
if (Game::config->GetValue("check_fdb") == "1" && !g_DatabaseChecksum.empty()) {
auto accountInfo = Database::Get()->GetAccountInfo(username.GetAsString());
if (!accountInfo) {
LOG("Client's account does not exist in the database, aborting connection.");
Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND);
return;
}
// Developers may skip this check
if (clientDatabaseChecksum.string != g_DatabaseChecksum) {
if (accountInfo->maxGmLevel < eGameMasterLevel::DEVELOPER) {
LOG("Client's database checksum does not match the server's, aborting connection.");
Stamps stamps;
stamps.Add(eStamps::PASSPORT_AUTH_ERROR, 1);
// Using the LoginResponse here since the UI is still in the login screen state
// and we have a way to send a message about the client mismatch.
AuthPackets::SendLoginResponse(
Game::server, sysAddr, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH,
Game::config->GetValue("cdclient_mismatch_message"), "", 0, "", stamps);
return;
} else {
AMFArrayValue args;
args.Insert("title", Game::config->GetValue("cdclient_mismatch_title"));
args.Insert("message", Game::config->GetValue("cdclient_mismatch_message"));
GameMessages::SendUIMessageServerToSingleClient("ToggleAnnounce", args, sysAddr);
LOG("Account (%s) with GmLevel (%s) does not have a matching FDB, but is a developer and will skip this check."
, username.GetAsString().c_str(), StringifiedEnum::ToString(accountInfo->maxGmLevel).data());
}
}
}
//Request the session info from Master:
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_SESSION_KEY);
bitStream.Write(username);
Game::server->SendToMaster(bitStream);
//Insert info into our pending list
TempSessionInfo info;
info.sysAddr = sysAddr;
info.hash = sessionKey.GetAsString();
g_PendingUsers[username.GetAsString()] = info;
}
};
struct CharacterListRequestPacket final : public WorldPackets::CharacterListRequest {
void Handle() override {
//We need to delete the entity first, otherwise the char list could delete it while it exists in the world!
if (Game::server->GetZoneID() != 0) {
auto user = UserManager::Instance()->GetUser(sysAddr);
if (!user || !user->GetLastUsedChar()) return;
Game::entityManager->DestroyEntity(user->GetLastUsedChar()->GetEntity());
}
//This loops prevents users who aren't authenticated to double-request the char list, which
//would make the login screen freeze sometimes.
if (g_PendingUsers.size() > 0) {
for (const auto& it : g_PendingUsers) {
if (it.second.sysAddr == sysAddr) {
return;
}
}
}
UserManager::Instance()->RequestCharacterList(sysAddr);
}
};
struct GameMessagePacket final : public WorldPackets::GameMessage {
void Handle() override {
auto isSender = CheatDetection::VerifyLwoobjidIsSender(
objectID,
sysAddr,
CheckType::Entity,
"Sending GM with a sending player that does not match their own. GM ID: %i",
static_cast<int32_t>(messageID)
);
if (isSender) GameMessageHandler::HandleMessage(data, sysAddr, objectID, messageID);
}
};
struct CharacterCreateRequestPacket final : public WorldPackets::CharacterCreateRequest {
void Handle() override {
UserManager::Instance()->CreateCharacter(sysAddr, *this);
}
};
struct CharacterLoginRequestPacket final : public WorldPackets::CharacterLoginRequest {
void Handle() override {
LOG("User is requesting to login with character %llu", playerID);
bool valid = CheatDetection::VerifyLwoobjidIsSender(
playerID,
sysAddr,
CheckType::User,
"Sending login request with a sending player that does not match their own. Player ID: %llu",
playerID
);
LOG("Login request for player %llu is %s", playerID, valid ? "valid" : "invalid");
if (!valid) return;
auto user = UserManager::Instance()->GetUser(sysAddr);
if (user) {
auto lastCharacter = user->GetLoggedInChar();
// This means we swapped characters and we need to remove the previous player from the container.
if (lastCharacter != playerID) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT);
bitStream.Write(lastCharacter);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
}
}
UserManager::Instance()->LoginCharacter(sysAddr, playerID);
}
};
struct CharacterDeleteRequestPacket final : public WorldPackets::CharacterDeleteRequest {
void Handle() override {
UserManager::Instance()->DeleteCharacter(sysAddr, *this);
}
};
struct CharacterRenameRequestPacket final : public WorldPackets::CharacterRenameRequest {
void Handle() override {
UserManager::Instance()->RenameCharacter(sysAddr, *this);
}
};
struct LevelLoadCompletePacket final : public WorldPackets::LevelLoadComplete {
void Handle() override {
LOG("Received level load complete from user.");
LoadPlayer(sysAddr);
}
};
struct PositionUpdatePacket final : public WorldPackets::PositionUpdate {
void Handle() override {
auto positionUpdate = update;
User* user = UserManager::Instance()->GetUser(sysAddr);
if (!user) {
LOG("Unable to get user to parse position update");
return;
}
if (const auto* const lastChar = user->GetLastUsedChar()) {
if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) {
entity->ProcessPositionUpdate(positionUpdate);
}
}
}
};
struct MailPacket final : public WorldPackets::MailPacket {
void Handle() override {
if (auto* const user = UserManager::Instance()->GetUser(sysAddr)) {
if (auto* const lastChar = user->GetLastUsedChar()) {
if (auto* const entity = lastChar->GetEntity()) {
Mail::HandleMail(data, sysAddr, entity);
}
}
}
}
};
struct RoutePacket final : public WorldPackets::RoutePacket {
void Handle() override {
//Yeet to chat
if (size > 20000) {
LOG("Tried to route a packet with a read size > 20000, so likely a false packet.");
return;
}
//We need to insert the player's objectID so the chat server can find who originated this request:
LWOOBJID objectID = 0;
auto user = UserManager::Instance()->GetUser(sysAddr);
if (user) {
const auto* const lastChar = user->GetLastUsedChar();
if (lastChar) objectID = lastChar->GetObjectID();
}
const auto routed = ToChat(objectID);
RakNet::BitStream bitStream;
routed.WritePacket(bitStream);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false);
}
};
struct StringCheckPacket final : public WorldPackets::StringCheck {
void Handle() override {
const auto receiver = GetNarrowReceiver();
const auto message = GetNarrowMessage();
// TODO: Find a good home for the logic in this case.
User* user = UserManager::Instance()->GetUser(sysAddr);
if (!user) {
LOG("Unable to get user to parse chat moderation request");
return;
}
auto* entity = PlayerManager::GetPlayer(sysAddr);
if (entity == nullptr) {
LOG("Unable to get player to parse chat moderation request");
return;
}
// Check if the player has restricted chat access
auto* character = entity->GetCharacter();
if (character->HasPermission(ePermissionMap::RestrictedChatAccess)) {
// Send a message to the player
ChatPackets::SendSystemMessage(
sysAddr,
u"This character has restricted chat access."
);
return;
}
bool isBestFriend = false;
if (chatLevel == 1) {
// Private chat
LWOOBJID idOfReceiver = LWOOBJID_EMPTY;
{
auto characterIdFetch = Database::Get()->GetCharacterInfo(receiver);
if (characterIdFetch) {
idOfReceiver = characterIdFetch->id;
}
}
const auto& bffMap = user->GetIsBestFriendMap();
if (bffMap.find(receiver) == bffMap.end() && idOfReceiver != LWOOBJID_EMPTY) {
auto bffInfo = Database::Get()->GetBestFriendStatus(entity->GetObjectID(), idOfReceiver);
if (bffInfo) {
isBestFriend = bffInfo->bestFriendStatus == 3;
}
if (isBestFriend) {
user->UpdateBestFriendValue(receiver, true);
}
} else if (bffMap.find(receiver) != bffMap.end()) {
isBestFriend = true;
}
}
const auto segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1));
bool bAllClean = segments.empty();
if (user->GetIsMuted()) {
bAllClean = false;
}
user->SetLastChatMessageApproved(bAllClean);
ClientPackets::ChatModerationString response;
response.requestAccepted = segments.empty(); // What DLU has always sent, even when bAllClean is false
response.requestID = requestID;
response.receiver = LUWString(receiver, 42);
response.rejectedSegments = segments;
response.Send(sysAddr);
}
};
struct GeneralChatMessagePacket final : public WorldPackets::GeneralChatMessage {
void Handle() override {
if (g_ChatDisabled) {
ChatPackets::SendMessageFail(sysAddr);
} else {
// TODO: Find a good home for the logic in this case.
User* user = UserManager::Instance()->GetUser(sysAddr);
if (!user) {
LOG("Unable to get user to parse chat message");
return;
}
const auto* const lastChar = user->GetLastUsedChar();
if (!lastChar) {
LOG("No last used character for chat message %i", user->GetAccountID());
return;
}
if (user->GetIsMuted()) {
lastChar->SendMuteNotice();
return;
}
std::string playerName = lastChar->GetName();
bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN;
bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(message), lastChar->GetGMLevel()).empty();
LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(message).c_str(), user->GetLastChatMessageApproved());
// Kept for moderation and chat bridges, including what the filter stopped (log_chat=0 turns it off)
if (Game::config->GetValue("log_chat") != "0") {
IChatLog::ChatMessage entry;
entry.time = static_cast<int64_t>(std::time(nullptr));
entry.channel = "zone";
entry.senderId = user->GetLoggedInChar();
entry.senderName = lastChar->GetName();
entry.accountId = user->GetAccountID();
entry.zoneId = Game::server->GetZoneID();
entry.instanceId = static_cast<uint32_t>(Game::server->GetInstanceID());
entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID();
entry.message = GeneralUtils::UTF16ToWTF8(message);
entry.blocked = !isOk;
try {
Database::Get()->InsertChatMessage(entry);
} catch (const std::exception& ex) {
LOG("Couldn't log a chat message: %s", ex.what());
}
}
if (!isOk) return;
if (!isOk && !isMythran) return;
std::string sMessage = GeneralUtils::UTF16ToWTF8(message);
LOG("%s: %s", playerName.c_str(), sMessage.c_str());
ChatPackets::SendChatMessage(sysAddr, chatChannel, playerName, user->GetLoggedInChar(), isMythran, message);
if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage);
}
}
};
struct HandleFunnessPacket final : public WorldPackets::HandleFunness {
void Handle() override {
//This means the client is running slower or faster than it should.
//Could be insane lag, but I'mma just YEET them as it's usually speedhacking.
//This is updated to now count the amount of times we've been caught "speedhacking" to kick with a delay
//This is hopefully going to fix the random disconnects people face sometimes.
CaughtFunness funness;
funness.cheatInfo = this->cheatInfo;
funness.cheatType = this->cheatType;
const auto [cheatType, cheatInfo] = funness;
LOG_DEBUG("Received cheat type %s with info %f", StringifiedEnum::ToString(cheatType).data(), cheatInfo);
const auto disabledCheats = Game::config->GetValue("disable_anti_speedhack") == "1";
User* user = UserManager::Instance()->GetUser(sysAddr);
if (!user) {
Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK);
return;
}
const auto* const character = user->GetLastUsedChar();
if (character) {
const auto dcUser = [](const User& user, const SystemAddress& sysAddr) {
if (user.GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) {
Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK);
}
};
if (cheatType == eFunnessTypes::DebuggerActive) {
LOG("Player %s was detected to be using a debugger (funness thread was %f seconds longer than expected deviation).", character->GetName().c_str(), cheatInfo);
// Immediately kick unless they are a gm
if (!disabledCheats) dcUser(*user, sysAddr);
} else if (cheatType == eFunnessTypes::FdbFailedChecksum) {
LOG("Player %s has failed the fdb checksum after passing it to sign into this server, highly likely cheating.", character->GetName().c_str());
// Immedately kick unless they are a gm
if (!disabledCheats) dcUser(*user, sysAddr);
} else if (cheatType == eFunnessTypes::RacingBoostTimeTooLong) {
if (cheatInfo == 0.0f) LOG("Player %s has enabled a speedboost for far too long (> 3.501 seconds).", character->GetName().c_str());
else if (cheatInfo == 1.0f) LOG("Unknown racing boost/speed variable was tampered with.");
else if (cheatInfo == 2.0f) LOG("Player vehicle top speed was tampered with.");
if (!disabledCheats) dcUser(*user, sysAddr);
} else if (cheatType == eFunnessTypes::SomeRacingManipCheat) {
if (cheatInfo >= 0.0f && cheatInfo <= 6.0f) LOG("Cheat RNG value A does not match what it should be %f.", cheatInfo);
else if (cheatInfo >= 7.0f && cheatInfo <= 10.0f) LOG("Cheat RNG value B does not match what it should be %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::Unknown_9) {
LOG("Racing cheat 9 detected with value %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::Unknown_10) {
LOG("Racing cheat 10 detected with value %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterPosLength) {
LOG("Detected pos length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterVelLength) {
LOG("Detected vel length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterGravityScale) {
LOG("Detected gravity scale difference of %f which is greater than the expected value of 0.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterRunMultiplier) {
LOG("Detected run multiplier difference of %f which is greater than the expected value 0.0f, not normal!", cheatInfo);
}
if (!disabledCheats && user->GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) user->UserOutOfSync(funness);
}
}
};
struct UIHelpTop5Packet final : public WorldPackets::UIHelpTop5 {
void Handle() override {
// TODO: Handle different languages in a nice way
// 0: en_US
// 1: pl_US
// 2: de_DE
// 3: en_GB
// TODO: Find a good home for the logic in this case.
auto* user = UserManager::Instance()->GetUser(sysAddr);
if (!user) return;
auto* character = user->GetLastUsedChar();
if (!character) return;
auto* entity = character->GetEntity();
if (!entity) return;
AMFArrayValue data;
// Summaries
data.Insert("Summary0", Game::config->GetValue("help_0_summary"));
data.Insert("Summary1", Game::config->GetValue("help_1_summary"));
data.Insert("Summary2", Game::config->GetValue("help_2_summary"));
data.Insert("Summary3", Game::config->GetValue("help_3_summary"));
data.Insert("Summary4", Game::config->GetValue("help_4_summary"));
// Descriptions
data.Insert("Description0", Game::config->GetValue("help_0_description"));
data.Insert("Description1", Game::config->GetValue("help_1_description"));
data.Insert("Description2", Game::config->GetValue("help_2_description"));
data.Insert("Description3", Game::config->GetValue("help_3_description"));
data.Insert("Description4", Game::config->GetValue("help_4_description"));
GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, "UIHelpTop5", data);
}
};
template<typename T>
std::unique_ptr<WorldPackets::WorldLUBitStream> Create() { return std::make_unique<T>(); }
const std::map<MessageType::World, std::function<std::unique_ptr<WorldPackets::WorldLUBitStream>()>> g_WorldHandlers = {
{ MessageType::World::VALIDATION, Create<ValidationPacket> },
{ MessageType::World::CHARACTER_LIST_REQUEST, Create<CharacterListRequestPacket> },
{ MessageType::World::GAME_MSG, Create<GameMessagePacket> },
{ MessageType::World::CHARACTER_CREATE_REQUEST, Create<CharacterCreateRequestPacket> },
{ MessageType::World::LOGIN_REQUEST, Create<CharacterLoginRequestPacket> },
{ MessageType::World::CHARACTER_DELETE_REQUEST, Create<CharacterDeleteRequestPacket> },
{ MessageType::World::CHARACTER_RENAME_REQUEST, Create<CharacterRenameRequestPacket> },
{ MessageType::World::LEVEL_LOAD_COMPLETE, Create<LevelLoadCompletePacket> },
{ MessageType::World::POSITION_UPDATE, Create<PositionUpdatePacket> },
{ MessageType::World::MAIL, Create<MailPacket> },
{ MessageType::World::ROUTE_PACKET, Create<RoutePacket> },
{ MessageType::World::STRING_CHECK, Create<StringCheckPacket> },
{ MessageType::World::GENERAL_CHAT_MESSAGE, Create<GeneralChatMessagePacket> },
{ MessageType::World::HANDLE_FUNNESS, Create<HandleFunnessPacket> },
{ MessageType::World::UI_HELP_TOP_5, Create<UIHelpTop5Packet> },
};
}
void HandlePacket(Packet* packet) {
if (packet->length < 1) return;
if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) {
@@ -1205,476 +1681,30 @@ void HandlePacket(Packet* packet) {
CINSTREAM;
LUBitStream luBitStream;
luBitStream.ReadHeader(inStream);
if (!luBitStream.ReadHeader(inStream)) return;
if (luBitStream.connectionType == ServiceType::COMMON) {
CommonPackets::Handle(inStream, packet->systemAddress, luBitStream.internalPacketID);
}
if (luBitStream.connectionType != ServiceType::WORLD) return;
LOG_DEBUG("Got world packet %s", StringifiedEnum::ToString(static_cast<MessageType::World>(luBitStream.internalPacketID)).data());
switch (static_cast<MessageType::World>(luBitStream.internalPacketID)) {
case MessageType::World::VALIDATION: {
CINSTREAM_SKIP_HEADER;
LUWString username;
inStream.Read(username);
const auto messageId = static_cast<MessageType::World>(luBitStream.internalPacketID);
LOG_DEBUG("Got world packet %s", StringifiedEnum::ToString(messageId).data());
LUWString sessionKey;
// sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird
inStream.Read(sessionKey);
LUString clientDatabaseChecksum(32);
inStream.Read(clientDatabaseChecksum);
// If the check is turned on, validate the client's database checksum.
if (Game::config->GetValue("check_fdb") == "1" && !g_DatabaseChecksum.empty()) {
auto accountInfo = Database::Get()->GetAccountInfo(username.GetAsString());
if (!accountInfo) {
LOG("Client's account does not exist in the database, aborting connection.");
Game::server->Disconnect(packet->systemAddress, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND);
return;
}
// Developers may skip this check
if (clientDatabaseChecksum.string != g_DatabaseChecksum) {
if (accountInfo->maxGmLevel < eGameMasterLevel::DEVELOPER) {
LOG("Client's database checksum does not match the server's, aborting connection.");
Stamps stamps;
stamps.Add(eStamps::PASSPORT_AUTH_ERROR, 1);
// Using the LoginResponse here since the UI is still in the login screen state
// and we have a way to send a message about the client mismatch.
AuthPackets::SendLoginResponse(
Game::server, packet->systemAddress, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH,
Game::config->GetValue("cdclient_mismatch_message"), "", 0, "", stamps);
return;
} else {
AMFArrayValue args;
args.Insert("title", Game::config->GetValue("cdclient_mismatch_title"));
args.Insert("message", Game::config->GetValue("cdclient_mismatch_message"));
GameMessages::SendUIMessageServerToSingleClient("ToggleAnnounce", args, packet->systemAddress);
LOG("Account (%s) with GmLevel (%s) does not have a matching FDB, but is a developer and will skip this check."
, username.GetAsString().c_str(), StringifiedEnum::ToString(accountInfo->maxGmLevel).data());
}
}
}
//Request the session info from Master:
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_SESSION_KEY);
bitStream.Write(username);
Game::server->SendToMaster(bitStream);
//Insert info into our pending list
TempSessionInfo info;
info.sysAddr = packet->systemAddress;
info.hash = sessionKey.GetAsString();
g_PendingUsers[username.GetAsString()] = info;
break;
}
case MessageType::World::CHARACTER_LIST_REQUEST: {
//We need to delete the entity first, otherwise the char list could delete it while it exists in the world!
if (Game::server->GetZoneID() != 0) {
auto user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user || !user->GetLastUsedChar()) return;
Game::entityManager->DestroyEntity(user->GetLastUsedChar()->GetEntity());
}
//This loops prevents users who aren't authenticated to double-request the char list, which
//would make the login screen freeze sometimes.
if (g_PendingUsers.size() > 0) {
for (const auto& it : g_PendingUsers) {
if (it.second.sysAddr == packet->systemAddress) {
return;
}
}
}
UserManager::Instance()->RequestCharacterList(packet->systemAddress);
break;
}
case MessageType::World::GAME_MSG: {
RakNet::BitStream bitStream(packet->data, packet->length, false);
uint64_t header;
LWOOBJID objectID;
MessageType::Game messageID;
bitStream.Read(header);
bitStream.Read(objectID);
bitStream.Read(messageID);
RakNet::BitStream dataStream;
bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits());
auto isSender = CheatDetection::VerifyLwoobjidIsSender(
objectID,
packet->systemAddress,
CheckType::Entity,
"Sending GM with a sending player that does not match their own. GM ID: %i",
static_cast<int32_t>(messageID)
);
if (isSender) GameMessageHandler::HandleMessage(dataStream, packet->systemAddress, objectID, messageID);
break;
}
case MessageType::World::CHARACTER_CREATE_REQUEST: {
UserManager::Instance()->CreateCharacter(packet->systemAddress, packet);
break;
}
case MessageType::World::LOGIN_REQUEST: {
CINSTREAM_SKIP_HEADER;
LWOOBJID playerID = 0;
inStream.Read(playerID);
LOG("User is requesting to login with character %llu", playerID);
bool valid = CheatDetection::VerifyLwoobjidIsSender(
playerID,
packet->systemAddress,
CheckType::User,
"Sending login request with a sending player that does not match their own. Player ID: %llu",
playerID
);
LOG("Login request for player %llu is %s", playerID, valid ? "valid" : "invalid");
if (!valid) return;
auto user = UserManager::Instance()->GetUser(packet->systemAddress);
if (user) {
auto lastCharacter = user->GetLoggedInChar();
// This means we swapped characters and we need to remove the previous player from the container.
if (lastCharacter != playerID) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT);
bitStream.Write(lastCharacter);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
}
}
UserManager::Instance()->LoginCharacter(packet->systemAddress, playerID);
break;
}
case MessageType::World::CHARACTER_DELETE_REQUEST: {
UserManager::Instance()->DeleteCharacter(packet->systemAddress, packet);
break;
}
case MessageType::World::CHARACTER_RENAME_REQUEST: {
UserManager::Instance()->RenameCharacter(packet->systemAddress, packet);
break;
}
case MessageType::World::LEVEL_LOAD_COMPLETE: {
LOG("Received level load complete from user.");
LoadPlayer(packet->systemAddress);
break;
}
case MessageType::World::POSITION_UPDATE: {
auto positionUpdate = ClientPackets::HandleClientPositionUpdate(packet);
User* user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user) {
LOG("Unable to get user to parse position update");
return;
}
if (const auto* const lastChar = user->GetLastUsedChar()) {
if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) {
entity->ProcessPositionUpdate(positionUpdate);
}
}
break;
}
case MessageType::World::MAIL: {
if (auto* const user = UserManager::Instance()->GetUser(packet->systemAddress)) {
if (auto* const lastChar = user->GetLastUsedChar()) {
if (auto* const entity = lastChar->GetEntity()) {
Mail::HandleMail(inStream, packet->systemAddress, entity);
}
}
}
break;
}
case MessageType::World::ROUTE_PACKET: {
//Yeet to chat
CINSTREAM_SKIP_HEADER;
uint32_t size = 0;
inStream.Read(size);
if (size > 20000) {
LOG("Tried to route a packet with a read size > 20000, so likely a false packet.");
return;
}
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, packet->data[14]);
//We need to insert the player's objectID so the chat server can find who originated this request:
LWOOBJID objectID = 0;
auto user = UserManager::Instance()->GetUser(packet->systemAddress);
if (user) {
const auto* const lastChar = user->GetLastUsedChar();
if (lastChar) objectID = lastChar->GetObjectID();
}
bitStream.Write(objectID);
//Now write the rest of the data:
auto data = inStream.GetData();
for (uint32_t i = 23; i - 23 < size && i < packet->length; ++i) {
bitStream.Write(data[i]);
}
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false);
break;
}
case MessageType::World::STRING_CHECK: {
auto request = ClientPackets::HandleChatModerationRequest(packet);
// TODO: Find a good home for the logic in this case.
User* user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user) {
LOG("Unable to get user to parse chat moderation request");
return;
}
auto* entity = PlayerManager::GetPlayer(packet->systemAddress);
if (entity == nullptr) {
LOG("Unable to get player to parse chat moderation request");
return;
}
// Check if the player has restricted chat access
auto* character = entity->GetCharacter();
if (character->HasPermission(ePermissionMap::RestrictedChatAccess)) {
// Send a message to the player
ChatPackets::SendSystemMessage(
packet->systemAddress,
u"This character has restricted chat access."
);
return;
}
bool isBestFriend = false;
if (request.chatLevel == 1) {
// Private chat
LWOOBJID idOfReceiver = LWOOBJID_EMPTY;
{
auto characterIdFetch = Database::Get()->GetCharacterInfo(request.receiver);
if (characterIdFetch) {
idOfReceiver = characterIdFetch->id;
}
}
const auto& bffMap = user->GetIsBestFriendMap();
if (bffMap.find(request.receiver) == bffMap.end() && idOfReceiver != LWOOBJID_EMPTY) {
auto bffInfo = Database::Get()->GetBestFriendStatus(entity->GetObjectID(), idOfReceiver);
if (bffInfo) {
isBestFriend = bffInfo->bestFriendStatus == 3;
}
if (isBestFriend) {
user->UpdateBestFriendValue(request.receiver, true);
}
} else if (bffMap.find(request.receiver) != bffMap.end()) {
isBestFriend = true;
}
}
const auto segments = Game::chatFilter->IsSentenceOkay(request.message, entity->GetGMLevel(), !(isBestFriend && request.chatLevel == 1));
bool bAllClean = segments.empty();
if (user->GetIsMuted()) {
bAllClean = false;
}
user->SetLastChatMessageApproved(bAllClean);
WorldPackets::SendChatModerationResponse(packet->systemAddress, bAllClean, request.requestID, request.receiver, segments);
break;
}
case MessageType::World::GENERAL_CHAT_MESSAGE: {
if (g_ChatDisabled) {
ChatPackets::SendMessageFail(packet->systemAddress);
} else {
auto chatMessage = ClientPackets::HandleChatMessage(packet);
// TODO: Find a good home for the logic in this case.
User* user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user) {
LOG("Unable to get user to parse chat message");
return;
}
const auto* const lastChar = user->GetLastUsedChar();
if (!lastChar) {
LOG("No last used character for chat message %i", user->GetAccountID());
return;
}
if (user->GetIsMuted()) {
lastChar->SendMuteNotice();
return;
}
std::string playerName = lastChar->GetName();
bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN;
bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), lastChar->GetGMLevel()).empty();
LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(chatMessage.message).c_str(), user->GetLastChatMessageApproved());
// Kept for moderation and chat bridges, including what the filter stopped (log_chat=0 turns it off)
if (Game::config->GetValue("log_chat") != "0") {
IChatLog::ChatMessage entry;
entry.time = static_cast<int64_t>(std::time(nullptr));
entry.channel = "zone";
entry.senderId = user->GetLoggedInChar();
entry.senderName = lastChar->GetName();
entry.accountId = user->GetAccountID();
entry.zoneId = Game::server->GetZoneID();
entry.instanceId = static_cast<uint32_t>(Game::server->GetInstanceID());
entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID();
entry.message = GeneralUtils::UTF16ToWTF8(chatMessage.message);
entry.blocked = !isOk;
try {
Database::Get()->InsertChatMessage(entry);
} catch (const std::exception& ex) {
LOG("Couldn't log a chat message: %s", ex.what());
}
}
if (!isOk) return;
if (!isOk && !isMythran) return;
std::string sMessage = GeneralUtils::UTF16ToWTF8(chatMessage.message);
LOG("%s: %s", playerName.c_str(), sMessage.c_str());
ChatPackets::SendChatMessage(packet->systemAddress, chatMessage.chatChannel, playerName, user->GetLoggedInChar(), isMythran, chatMessage.message);
if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage);
}
break;
}
case MessageType::World::HANDLE_FUNNESS: {
//This means the client is running slower or faster than it should.
//Could be insane lag, but I'mma just YEET them as it's usually speedhacking.
//This is updated to now count the amount of times we've been caught "speedhacking" to kick with a delay
//This is hopefully going to fix the random disconnects people face sometimes.
CINSTREAM_SKIP_HEADER;
CaughtFunness funness;
inStream.Read(funness.cheatInfo);
inStream.Read(funness.cheatType);
const auto [cheatType, cheatInfo] = funness;
LOG_DEBUG("Received cheat type %s with info %f", StringifiedEnum::ToString(cheatType).data(), cheatInfo);
const auto disabledCheats = Game::config->GetValue("disable_anti_speedhack") == "1";
User* user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user) {
Game::server->Disconnect(packet->systemAddress, eServerDisconnectIdentifiers::KICK);
return;
}
const auto* const character = user->GetLastUsedChar();
if (character) {
const auto dcUser = [](const User& user, const SystemAddress& sysAddr) {
if (user.GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) {
Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK);
}
};
if (cheatType == eFunnessTypes::DebuggerActive) {
LOG("Player %s was detected to be using a debugger (funness thread was %f seconds longer than expected deviation).", character->GetName().c_str(), cheatInfo);
// Immediately kick unless they are a gm
if (!disabledCheats) dcUser(*user, packet->systemAddress);
} else if (cheatType == eFunnessTypes::FdbFailedChecksum) {
LOG("Player %s has failed the fdb checksum after passing it to sign into this server, highly likely cheating.", character->GetName().c_str());
// Immedately kick unless they are a gm
if (!disabledCheats) dcUser(*user, packet->systemAddress);
} else if (cheatType == eFunnessTypes::RacingBoostTimeTooLong) {
if (cheatInfo == 0.0f) LOG("Player %s has enabled a speedboost for far too long (> 3.501 seconds).", character->GetName().c_str());
else if (cheatInfo == 1.0f) LOG("Unknown racing boost/speed variable was tampered with.");
else if (cheatInfo == 2.0f) LOG("Player vehicle top speed was tampered with.");
if (!disabledCheats) dcUser(*user, packet->systemAddress);
} else if (cheatType == eFunnessTypes::SomeRacingManipCheat) {
if (cheatInfo >= 0.0f && cheatInfo <= 6.0f) LOG("Cheat RNG value A does not match what it should be %f.", cheatInfo);
else if (cheatInfo >= 7.0f && cheatInfo <= 10.0f) LOG("Cheat RNG value B does not match what it should be %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::Unknown_9) {
LOG("Racing cheat 9 detected with value %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::Unknown_10) {
LOG("Racing cheat 10 detected with value %f.", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterPosLength) {
LOG("Detected pos length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterVelLength) {
LOG("Detected vel length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterGravityScale) {
LOG("Detected gravity scale difference of %f which is greater than the expected value of 0.0f, not normal!", cheatInfo);
} else if (cheatType == eFunnessTypes::CharacterRunMultiplier) {
LOG("Detected run multiplier difference of %f which is greater than the expected value 0.0f, not normal!", cheatInfo);
}
if (!disabledCheats && user->GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) user->UserOutOfSync(funness);
}
break;
}
case MessageType::World::UI_HELP_TOP_5: {
auto language = ClientPackets::SendTop5HelpIssues(packet);
// TODO: Handle different languages in a nice way
// 0: en_US
// 1: pl_US
// 2: de_DE
// 3: en_GB
// TODO: Find a good home for the logic in this case.
auto* user = UserManager::Instance()->GetUser(packet->systemAddress);
if (!user) return;
auto* character = user->GetLastUsedChar();
if (!character) return;
auto* entity = character->GetEntity();
if (!entity) return;
AMFArrayValue data;
// Summaries
data.Insert("Summary0", Game::config->GetValue("help_0_summary"));
data.Insert("Summary1", Game::config->GetValue("help_1_summary"));
data.Insert("Summary2", Game::config->GetValue("help_2_summary"));
data.Insert("Summary3", Game::config->GetValue("help_3_summary"));
data.Insert("Summary4", Game::config->GetValue("help_4_summary"));
// Descriptions
data.Insert("Description0", Game::config->GetValue("help_0_description"));
data.Insert("Description1", Game::config->GetValue("help_1_description"));
data.Insert("Description2", Game::config->GetValue("help_2_description"));
data.Insert("Description3", Game::config->GetValue("help_3_description"));
data.Insert("Description4", Game::config->GetValue("help_4_description"));
GameMessages::SendUIMessageServerToSingleClient(entity, packet->systemAddress, "UIHelpTop5", data);
break;
}
default:
const auto messageId = *reinterpret_cast<MessageType::World*>(&packet->data[3]);
const auto handler = g_WorldHandlers.find(messageId);
if (handler == g_WorldHandlers.end()) {
const std::string_view messageIdString = StringifiedEnum::ToString(messageId);
LOG("Unknown world packet received: %4i, %s", messageId, messageIdString.data());
return;
}
auto request = handler->second();
request->sysAddr = packet->systemAddress;
if (!request->Deserialize(inStream)) {
LOG("Failed to read world packet %s from %s", StringifiedEnum::ToString(messageId).data(), packet->systemAddress.ToString());
return;
}
request->Handle();
}
void WorldShutdownProcess(uint32_t zoneId) {

View File

@@ -159,9 +159,14 @@ Every non-game-message packet is an `LUBitStream` subclass. The base owns the fu
- Each service has one dispatch function with a `std::map<MessageType::X, factory>`, exactly like
`Mail::HandleMail`: read the id, create, set context, `Deserialize` (log and drop on failure), `Handle`. Each
server's `HandlePacket` only routes by `ServiceType`.
- Structs live in their service's namespace and file: `AuthPackets`, `ChatPackets`, `ClientPackets`,
`CommonPackets` (new, for `ServiceType::COMMON`), `MasterPackets`, `WorldPackets` in `dNet/`; Mail stays in
`dGame/dUtilities/Mail.*`. Handlers that need game or server state stay in the server that owns that state
- **One file pair per `ServiceType`; a packet goes in the file of the `ServiceType` written in its header**, in
that namespace: `CommonPackets` (COMMON), `AuthPackets` (AUTH), `ChatPackets` (CHAT), `WorldPackets` (WORLD:
what a client sends to a world server), `ClientPackets` (CLIENT: what any server sends to the client),
`MasterPackets` (MASTER), all in `dNet/`. So the auth server's login response is in `ClientPackets`, and a
world server's message to chat is in `ChatPackets`. Anything used by several services (for example `Stamps`,
which rides in auth, master and client packets, or generic helpers) gets its own small file named for what it
is (`dNet/Stamps.h`), not one service's file. A packet found in the wrong file is moved (a pure move, same
bytes). Mail stays in `dGame/dUtilities/Mail.*`. Handlers that need game or server state stay in the server that owns that state
(`dChatServer`, `dMasterServer`, `dWorldServer`, `dAuthServer`) and are attached to the struct by overriding
`Handle` in that server's translation unit.
- Fixed-width strings use `LUString`/`LUWString` with the width spelled out (`LUWString password(41)`).

View File

@@ -1,5 +1,6 @@
SET(DNET_TESTS
"CommonAuthPacketsTests.cpp")
"CommonAuthPacketsTests.cpp"
"WorldPacketsTests.cpp")
# Get the folder name and prepend it to the files above
get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME)

View File

@@ -0,0 +1,543 @@
#ifndef WORLDPACKETSLEGACY_H
#define WORLDPACKETSLEGACY_H
// FROZEN ORACLE - DO NOT EDIT.
// Verbatim copies of the hand written code that WorldPackets / the ClientPackets world responses replaced, from
// before the conversion: every WorldPackets::Send* function and HTTPMonitorInfo (dNet/WorldPackets.cpp), the four
// ClientPackets parse functions (dNet/ClientPackets.cpp), and, below them, the parts of UserManager.cpp and
// WorldServer.cpp's HandlePacket that read or wrote packets inline (each copied verbatim into a function that
// returns what was read, or the packet that was built, instead of acting on it).
// Only the namespace changed. The byte-equality tests run the same inputs through these and the new structs.
#include "BitStreamUtils.h"
#include "dCommonVars.h"
#include "dServer.h"
#include "eCharacterCreationResponse.h"
#include "eFunnessTypes.h"
#include "eGameMasterLevel.h"
#include "eRenameResponse.h"
#include "Game.h"
#include "LDFFormat.h"
#include "Logger.h"
#include "MessageType/Chat.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "PositionUpdate.h"
#include "ServiceType.h"
#include "ZCompression.h"
#include <cassert>
#include <memory>
#include <ranges>
#include <set>
#include <string>
#include <vector>
namespace LegacyWorldPackets {
struct HTTPMonitorInfo {
uint16_t port = 80;
bool openWeb = false;
bool supportsSum = false;
bool supportsDetail = false;
bool supportsWho = false;
bool supportsObjects = false;
void Serialize(RakNet::BitStream &bitstream) const;
};
inline void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(port);
bitStream.Write<uint8_t>(openWeb);
bitStream.Write<uint8_t>(supportsSum);
bitStream.Write<uint8_t>(supportsDetail);
bitStream.Write<uint8_t>(supportsWho);
bitStream.Write<uint8_t>(supportsObjects);
}
inline void SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE);
bitStream.Write<uint16_t>(zone.GetMapID());
bitStream.Write<uint16_t>(zone.GetInstanceID());
//bitStream.Write<uint32_t>(zone.GetCloneID());
bitStream.Write(0);
bitStream.Write(checksum);
bitStream.Write<uint16_t>(0); // ??
bitStream.Write(x);
bitStream.Write(y);
bitStream.Write(z);
bitStream.Write<uint32_t>(0); // Change this to eventually use 4 on activity worlds
SEND_PACKET;
}
inline void SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE);
bitStream.Write(response);
SEND_PACKET;
}
inline void SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE);
bitStream.Write(response);
SEND_PACKET;
}
inline void SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE);
bitStream.Write<uint8_t>(response);
SEND_PACKET;
}
inline void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD);
bitStream.Write(LUString(serverIP));
bitStream.Write<uint16_t>(serverPort);
bitStream.Write<uint8_t>(mythranShift);
SEND_PACKET;
}
inline void SendServerState(const SystemAddress& sysAddr) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SERVER_STATES);
bitStream.Write<uint8_t>(1); //If the server is receiving this request, it probably is ready anyway.
SEND_PACKET;
}
inline void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID) {
using namespace std;
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER);
RakNet::BitStream data;
LwoNameValue ldfData;
ldfData.Insert<LWOOBJID>(u"objid", player);
ldfData.Insert<LOT>(u"template", 1);
ldfData.Insert<string>(u"xmlData", xmlData);
ldfData.Insert<u16string>(u"name", username);
ldfData.Insert<int32_t>(u"gmlevel", static_cast<int32_t>(gm));
ldfData.Insert<int32_t>(u"chatmode", static_cast<int32_t>(gm));
ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", cloneID);
data.Write<uint32_t>(ldfData.values.size());
for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data);
//Compress the data before sending:
const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed());
auto compressedData = std::make_unique<uint8_t[]>(reservedSize);
size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize);
assert(size <= reservedSize);
bitStream.Write<uint32_t>(size + 9); //size of data + header bytes (8)
bitStream.Write<uint8_t>(1); //compressed boolean, true
bitStream.Write<uint32_t>(data.GetNumberOfBytesUsed());
bitStream.Write<uint32_t>(size);
/**
* In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap
* and then compress the data. In the off chance that the compression actually increases the size,
* an assertion is done to prevent bad data from being saved or sent.
*/
#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'.
bitStream.WriteAlignedBytes(compressedData.get(), size);
#pragma warning(default:6385)
SEND_PACKET;
LOG("Sent CreateCharacter for ID: %llu", player);
}
inline void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING);
bitStream.Write<uint8_t>(unacceptedItems.empty()); // Is sentence ok?
bitStream.Write<uint16_t>(0x16); // Source ID, unknown
bitStream.Write<uint8_t>(requestID); // request ID
bitStream.Write<char>(0); // chat mode
bitStream.Write(LUWString(receiver, 42)); // receiver name
for (auto it : unacceptedItems) {
bitStream.Write<uint8_t>(it.first); // start index
bitStream.Write<uint8_t>(it.second); // length
}
for (int i = unacceptedItems.size(); 64 > i; i++) {
bitStream.Write<uint16_t>(0);
}
SEND_PACKET;
}
inline void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE);
bitStream.Write<uint8_t>(success);
bitStream.Write(static_cast<uint16_t>(highestLevel));
bitStream.Write(static_cast<uint16_t>(prevLevel));
bitStream.Write(static_cast<uint16_t>(newLevel));
SEND_PACKET;
}
inline void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE);
info.Serialize(bitStream);
SEND_PACKET;
}
inline void SendDebugOuput(const SystemAddress& sysAddr, const std::string& data) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT);
bitStream.Write<uint32_t>(data.size());
bitStream.Write(data);
SEND_PACKET;
}
struct ChatMessage {
uint8_t chatChannel = 0;
uint16_t unknown = 0;
std::u16string message;
};
struct ChatModerationRequest {
uint8_t chatLevel = 0;
uint8_t requestID = 0;
std::string receiver;
std::string message;
};
inline ChatMessage HandleChatMessage(Packet* packet) {
CINSTREAM_SKIP_HEADER;
ChatMessage message;
int32_t messageLength{};
inStream.Read(message.chatChannel);
inStream.Read(message.unknown);
inStream.Read(messageLength);
if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message;
for (int32_t i = 0; i < (messageLength - 1); ++i) {
char16_t character;
inStream.Read(character);
message.message.push_back(character);
}
return message;
}
inline PositionUpdate HandleClientPositionUpdate(Packet* packet) {
PositionUpdate update;
CINSTREAM_SKIP_HEADER;
inStream.Read(update.position.x);
inStream.Read(update.position.y);
inStream.Read(update.position.z);
inStream.Read(update.rotation.x);
inStream.Read(update.rotation.y);
inStream.Read(update.rotation.z);
inStream.Read(update.rotation.w);
inStream.Read(update.onGround);
inStream.Read(update.onRail);
bool velocityFlag = false;
inStream.Read(velocityFlag);
if (velocityFlag) {
inStream.Read(update.velocity.x);
inStream.Read(update.velocity.y);
inStream.Read(update.velocity.z);
}
bool angVelocityFlag = false;
inStream.Read(angVelocityFlag);
if (angVelocityFlag) {
inStream.Read(update.angularVelocity.x);
inStream.Read(update.angularVelocity.y);
inStream.Read(update.angularVelocity.z);
}
// TODO figure out how to use these. Ignoring for now, but reading in if they exist.
bool hasLocalSpaceInfo{};
if (inStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) {
inStream.Read(update.localSpaceInfo.objectId);
inStream.Read(update.localSpaceInfo.position.x);
inStream.Read(update.localSpaceInfo.position.y);
inStream.Read(update.localSpaceInfo.position.z);
bool hasLinearVelocity = false;
if (inStream.Read(hasLinearVelocity) && hasLinearVelocity) {
inStream.Read(update.localSpaceInfo.linearVelocity.x);
inStream.Read(update.localSpaceInfo.linearVelocity.y);
inStream.Read(update.localSpaceInfo.linearVelocity.z);
}
}
bool hasRemoteInputInfo{};
if (inStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) {
inStream.Read(update.remoteInputInfo.m_RemoteInputX);
inStream.Read(update.remoteInputInfo.m_RemoteInputY);
inStream.Read(update.remoteInputInfo.m_IsPowersliding);
inStream.Read(update.remoteInputInfo.m_IsModified);
}
return update;
}
inline ChatModerationRequest HandleChatModerationRequest(Packet* packet) {
CINSTREAM_SKIP_HEADER;
ChatModerationRequest request;
inStream.Read(request.chatLevel);
inStream.Read(request.requestID);
for (uint32_t i = 0; i < 42; ++i) {
uint16_t character;
inStream.Read(character);
request.receiver.push_back(static_cast<uint8_t>(character));
}
if (!request.receiver.empty()) {
if (std::string(request.receiver.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are
request.receiver = std::string(request.receiver.c_str() + 4, request.receiver.size() - 4);
}
}
uint16_t messageLength;
inStream.Read(messageLength);
if (messageLength > MAX_MESSAGE_LENGTH) return request;
for (uint32_t i = 0; i < messageLength; ++i) {
uint16_t character;
inStream.Read(character);
request.message.push_back(static_cast<uint8_t>(character));
}
return request;
}
inline int32_t SendTop5HelpIssues(Packet* packet) {
CINSTREAM_SKIP_HEADER;
int32_t language = 0;
inStream.Read(language);
return language;
}
// ---- UserManager::RequestCharacterList, the part that wrote the packet (characters[i] is any type with
// Character's getters) ----
template<typename CharacterT>
inline void SendCharacterList(const SystemAddress& sysAddr, std::vector<CharacterT*> characters) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE);
bitStream.Write<uint8_t>(characters.size());
bitStream.Write<uint8_t>(0); //TODO: Pick the most recent played index. character index in front, just picking 0
for (uint32_t i = 0; i < characters.size(); ++i) {
bitStream.Write(characters[i]->GetObjectID());
bitStream.Write<uint32_t>(0);
bitStream.Write(LUWString(characters[i]->GetName()));
bitStream.Write(LUWString(characters[i]->GetUnapprovedName()));
bitStream.Write<uint8_t>(characters[i]->GetNameRejected());
bitStream.Write<uint8_t>(false);
bitStream.Write(LUString("", 10));
bitStream.Write(characters[i]->GetShirtColor());
bitStream.Write(characters[i]->GetShirtStyle());
bitStream.Write(characters[i]->GetPantsColor());
bitStream.Write(characters[i]->GetHairStyle());
bitStream.Write(characters[i]->GetHairColor());
bitStream.Write(characters[i]->GetLeftHand());
bitStream.Write(characters[i]->GetRightHand());
bitStream.Write(characters[i]->GetEyebrows());
bitStream.Write(characters[i]->GetEyes());
bitStream.Write(characters[i]->GetMouth());
bitStream.Write<uint32_t>(0);
bitStream.Write<uint16_t>(characters[i]->GetZoneID());
bitStream.Write<uint16_t>(characters[i]->GetZoneInstance());
bitStream.Write(characters[i]->GetZoneClone());
bitStream.Write(characters[i]->GetLastLogin());
const auto& equippedItems = characters[i]->GetEquippedItems();
bitStream.Write<uint16_t>(equippedItems.size());
for (uint32_t j = 0; j < equippedItems.size(); ++j) {
bitStream.Write(equippedItems[j]);
}
}
SEND_PACKET;
}
// ---- UserManager::CreateCharacter, the reads ----
struct CreateCharacterRead {
LUWString LUWStringName;
uint32_t firstNameIndex;
uint32_t middleNameIndex;
uint32_t lastNameIndex;
uint32_t shirtColor;
uint32_t shirtStyle;
uint32_t pantsColor;
uint32_t hairStyle;
uint32_t hairColor;
uint32_t lh;
uint32_t rh;
uint32_t eyebrows;
uint32_t eyes;
uint32_t mouth;
};
inline CreateCharacterRead ReadCreateCharacter(Packet* packet) {
CreateCharacterRead r{};
auto& [LUWStringName, firstNameIndex, middleNameIndex, lastNameIndex, shirtColor, shirtStyle, pantsColor, hairStyle, hairColor, lh, rh, eyebrows, eyes, mouth] = r;
CINSTREAM_SKIP_HEADER;
inStream.Read(LUWStringName);
inStream.Read(firstNameIndex);
inStream.Read(middleNameIndex);
inStream.Read(lastNameIndex);
inStream.IgnoreBytes(9);
inStream.Read(shirtColor);
inStream.Read(shirtStyle);
inStream.Read(pantsColor);
inStream.Read(hairStyle);
inStream.Read(hairColor);
inStream.Read(lh);
inStream.Read(rh);
inStream.Read(eyebrows);
inStream.Read(eyes);
inStream.Read(mouth);
return r;
}
// ---- UserManager::DeleteCharacter, the read ----
inline LWOOBJID ReadDeleteCharacter(Packet* packet) {
CINSTREAM_SKIP_HEADER;
LWOOBJID objectID;
inStream.Read(objectID);
return objectID;
}
// ---- UserManager::RenameCharacter, the reads ----
inline std::pair<LWOOBJID, std::string> ReadRenameCharacter(Packet* packet) {
CINSTREAM_SKIP_HEADER;
LWOOBJID objectID;
inStream.Read(objectID);
LUWString LUWStringName;
inStream.Read(LUWStringName);
auto newName = LUWStringName.GetAsString();
return { objectID, newName };
}
// ---- WorldServer HandlePacket VALIDATION, the reads ----
struct ValidationRead {
std::string username;
std::string sessionKey;
std::string checksum;
};
inline ValidationRead ReadValidation(Packet* packet) {
CINSTREAM_SKIP_HEADER;
LUWString username;
inStream.Read(username);
LUWString sessionKey;
// sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird
inStream.Read(sessionKey);
LUString clientDatabaseChecksum(32);
inStream.Read(clientDatabaseChecksum);
return { username.GetAsString(), sessionKey.GetAsString(), clientDatabaseChecksum.string };
}
// ---- WorldServer HandlePacket LOGIN_REQUEST, the read ----
inline LWOOBJID ReadLoginRequest(Packet* packet) {
CINSTREAM_SKIP_HEADER;
LWOOBJID playerID = 0;
inStream.Read(playerID);
return playerID;
}
// ---- WorldServer HandlePacket GAME_MSG, the reads ----
struct GameMessageRead {
LWOOBJID objectID;
MessageType::Game messageID;
std::vector<uint8_t> data;
uint32_t dataBits;
};
inline GameMessageRead ReadGameMessage(Packet* packet) {
RakNet::BitStream bitStream(packet->data, packet->length, false);
uint64_t header;
LWOOBJID objectID;
MessageType::Game messageID;
bitStream.Read(header);
bitStream.Read(objectID);
bitStream.Read(messageID);
RakNet::BitStream dataStream;
bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits());
return { objectID, messageID, { dataStream.GetData(), dataStream.GetData() + dataStream.GetNumberOfBytesUsed() }, static_cast<uint32_t>(dataStream.GetNumberOfBitsUsed()) };
}
// ---- WorldServer HandlePacket ROUTE_PACKET: the packet it built for the chat server (it then sent it with
// Game::chatServer->Send); objectID is what it looked up for the sender. Returns false where it returned. ----
inline bool BuildRoutePacket(Packet* packet, LWOOBJID objectID, RakNet::BitStream& bitStream) {
//Yeet to chat
CINSTREAM_SKIP_HEADER;
uint32_t size = 0;
inStream.Read(size);
if (size > 20000) {
LOG("Tried to route a packet with a read size > 20000, so likely a false packet.");
return false;
}
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, packet->data[14]);
bitStream.Write(objectID);
//Now write the rest of the data:
auto data = inStream.GetData();
for (uint32_t i = 23; i - 23 < size && i < packet->length; ++i) {
bitStream.Write(data[i]);
}
return true;
}
// ---- WorldServer HandlePacket HANDLE_FUNNESS, the reads ----
inline CaughtFunness ReadFunness(Packet* packet) {
CINSTREAM_SKIP_HEADER;
CaughtFunness funness;
inStream.Read(funness.cheatInfo);
inStream.Read(funness.cheatType);
return funness;
}
}
#endif // WORLDPACKETSLEGACY_H

View File

@@ -0,0 +1,719 @@
#include "ChatPackets.h"
#include "ClientPackets.h"
#include "GameDependencies.h"
#include "PacketTestUtils.h"
#include "WorldPackets.h"
#include "Legacy/WorldPacketsLegacy.h"
#include "eCharacterCreationResponse.h"
#include "eGameMasterLevel.h"
#include "eRenameResponse.h"
#include "magic_enum.hpp"
#include <functional>
#include <limits>
#include <string>
#include <vector>
#include <gtest/gtest.h>
using namespace PacketTestUtils;
namespace {
SystemAddress TestAddress() {
SystemAddress address;
address.binaryAddress = 0x0100007F;
address.port = 1234;
return address;
}
void ExpectSameSend(const std::function<void(const SystemAddress&)>& legacy, const LUBitStream& packet) {
const auto sysAddr = TestAddress();
const auto expected = Capture([&] { legacy(sysAddr); });
const auto actual = Capture([&] { packet.Send(sysAddr); });
ASSERT_EQ(expected.size(), 1);
ASSERT_EQ(actual.size(), 1);
EXPECT_PACKET_EQ(FromCapture(expected[0]), FromCapture(actual[0]));
EXPECT_EQ(expected[0].sysAddr, actual[0].sysAddr);
EXPECT_EQ(expected[0].broadcast, actual[0].broadcast);
}
template<typename T>
T RoundTrip(const T& packet) {
RakNet::BitStream first;
packet.WritePacket(first);
T copy;
EXPECT_TRUE(copy.ReadHeader(first));
EXPECT_TRUE(copy.Deserialize(first));
EXPECT_EQ(first.GetNumberOfUnreadBits(), 0);
RakNet::BitStream second;
copy.WritePacket(second);
EXPECT_PACKET_EQ(FromBitStream(first), FromBitStream(second));
return copy;
}
template<typename T>
void ExpectTruncatedFails(const T& packet, uint32_t keepAtLeast = 8) {
RakNet::BitStream full;
packet.WritePacket(full);
const auto bytes = full.GetNumberOfBytesUsed();
for (uint32_t cut = keepAtLeast; cut < bytes; cut++) {
RakNet::BitStream truncated(full.GetData(), cut, true);
T copy;
ASSERT_TRUE(copy.ReadHeader(truncated));
EXPECT_FALSE(copy.Deserialize(truncated)) << "cut at " << cut << " of " << bytes;
}
}
// A received packet as the servers get it, from bytes written by a struct.
struct ReceivedPacket {
RakNet::BitStream bytes;
Packet packet{};
explicit ReceivedPacket(const LUBitStream& from) {
from.WritePacket(bytes);
packet.systemAddress = TestAddress();
packet.data = bytes.GetData();
packet.length = bytes.GetNumberOfBytesUsed();
packet.bitSize = bytes.GetNumberOfBitsUsed();
}
};
// Reads a struct from a received packet the way the dispatchers do.
template<typename T>
bool Read(ReceivedPacket& received, T& out) {
RakNet::BitStream inStream(received.packet.data, received.packet.length, false);
if (!out.ReadHeader(inStream)) return false;
return out.Deserialize(inStream);
}
const std::vector<std::string> g_Strings = { "", "a", "Hello World", "127.0.0.1", std::string(32, 'x'), std::string(33, 'y'), std::string(300, 'z') };
const std::vector<LWOOBJID> g_ObjectIDs = { 0, 1, 1152921508901814000, -1, std::numeric_limits<LWOOBJID>::max() };
struct FakeCharacter {
LWOOBJID objectID{};
std::string name;
std::string unapprovedName;
bool nameRejected{};
uint32_t shirtColor{}, shirtStyle{}, pantsColor{}, hairStyle{}, hairColor{}, leftHand{}, rightHand{}, eyebrows{}, eyes{}, mouth{};
uint32_t zoneID{}, zoneInstance{}, zoneClone{};
uint64_t lastLogin{};
std::vector<LOT> equippedItems;
LWOOBJID GetObjectID() const { return objectID; }
const std::string& GetName() const { return name; }
const std::string& GetUnapprovedName() const { return unapprovedName; }
bool GetNameRejected() const { return nameRejected; }
uint32_t GetShirtColor() const { return shirtColor; }
uint32_t GetShirtStyle() const { return shirtStyle; }
uint32_t GetPantsColor() const { return pantsColor; }
uint32_t GetHairStyle() const { return hairStyle; }
uint32_t GetHairColor() const { return hairColor; }
uint32_t GetLeftHand() const { return leftHand; }
uint32_t GetRightHand() const { return rightHand; }
uint32_t GetEyebrows() const { return eyebrows; }
uint32_t GetEyes() const { return eyes; }
uint32_t GetMouth() const { return mouth; }
uint32_t GetZoneID() const { return zoneID; }
uint32_t GetZoneInstance() const { return zoneInstance; }
uint32_t GetZoneClone() const { return zoneClone; }
uint64_t GetLastLogin() const { return lastLogin; }
const std::vector<LOT>& GetEquippedItems() const { return equippedItems; }
};
}
class WorldPacketsTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
// ---- Server -> client ----
TEST_F(WorldPacketsTests, LoadStaticZoneMatchesLegacy) {
for (const auto& zone : { LWOZONEID(1000, 0, 0), LWOZONEID(1100, 7, 1234), LWOZONEID(0xFFFF, 0xFFFF, 0xFFFFFFFF) }) {
for (const auto& [x, y, z] : { std::tuple{ 0.0f, 0.0f, 0.0f }, std::tuple{ -1.5f, 200.25f, 1e30f } }) {
for (const uint32_t checksum : { 0u, 0x20b8087cu, 0xFFFFFFFFu }) {
ClientPackets::LoadStaticZone packet;
packet.mapID = zone.GetMapID();
packet.instanceID = zone.GetInstanceID();
packet.cloneID = 0; // what the world server sends
packet.mapChecksum = checksum;
packet.playerPosition = NiPoint3(x, y, z);
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendLoadStaticZone(a, x, y, z, checksum, zone); }, packet);
EXPECT_EQ(RoundTrip(packet).mapChecksum, checksum);
}
}
}
ClientPackets::LoadStaticZone packet;
packet.mapID = 1100;
packet.instanceID = 2;
packet.mapChecksum = 0x11223344;
packet.playerPosition = NiPoint3(1.0f, 2.0f, 3.0f);
RakNet::BitStream bytes;
packet.WritePacket(bytes);
// 0x53 | CLIENT (5) | LOAD_STATIC_ZONE (2) | pad | map | instance | clone | checksum | editor u8 u8 | x y z | instance type
EXPECT_PACKET_EQ(FromHex("53 05 00 02 00 00 00 00 4c 04 02 00 00 00 00 00 44 33 22 11 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00 00 00"), FromBitStream(bytes));
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, SmallResponsesMatchLegacy) {
for (const auto response : magic_enum::enum_values<eCharacterCreationResponse>()) {
ClientPackets::CharacterCreateResponse packet;
packet.response = response;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterCreationResponse(a, response); }, packet);
EXPECT_EQ(RoundTrip(packet).response, response);
}
for (const auto response : magic_enum::enum_values<eRenameResponse>()) {
ClientPackets::CharacterRenameResponse packet;
packet.response = response;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterRenameResponse(a, response); }, packet);
EXPECT_EQ(RoundTrip(packet).response, response);
}
for (const bool success : { false, true }) {
ClientPackets::DeleteCharacterResponse packet;
packet.success = success;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterDeleteResponse(a, success); }, packet);
EXPECT_EQ(RoundTrip(packet).success, success);
}
{
ClientPackets::ServerStates packet;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendServerState(a); }, packet);
RoundTrip(packet);
}
ClientPackets::CharacterCreateResponse created;
created.response = eCharacterCreationResponse::CUSTOM_NAME_IN_USE;
RakNet::BitStream bytes;
created.WritePacket(bytes);
EXPECT_PACKET_EQ(FromHex("53 05 00 07 00 00 00 00 04"), FromBitStream(bytes));
}
TEST_F(WorldPacketsTests, TransferToWorldMatchesLegacy) {
for (const auto& ip : g_Strings) {
for (const uint32_t port : { 0u, 2001u, 0xFFFFu, 0x12345u }) {
for (const bool mythranShift : { false, true }) {
ClientPackets::TransferToWorld packet;
packet.serverIP = LUString(ip);
packet.serverPort = port;
packet.mythranShift = mythranShift;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendTransferToWorld(a, ip, port, mythranShift); }, packet);
const auto copy = RoundTrip(packet);
EXPECT_EQ(copy.serverPort, static_cast<uint16_t>(port));
EXPECT_EQ(copy.mythranShift, mythranShift);
}
}
}
ClientPackets::TransferToWorld packet;
packet.serverIP = LUString("1.2");
packet.serverPort = 0x0102;
packet.mythranShift = true;
RakNet::BitStream bytes;
packet.WritePacket(bytes);
std::string zeros;
for (int i = 0; i < 30; i++) zeros += " 00";
EXPECT_PACKET_EQ(FromHex("53 05 00 0e 00 00 00 00 31 2e 32" + zeros + " 02 01 01"), FromBitStream(bytes));
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, MakeGMResponseMatchesLegacy) {
const auto levels = magic_enum::enum_values<eGameMasterLevel>();
for (const bool success : { false, true }) {
for (const auto highest : levels) {
for (const auto previous : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) {
ClientPackets::MakeGMResponse packet;
packet.success = success;
packet.highestLevel = highest;
packet.previousLevel = previous;
packet.newLevel = highest;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendGMLevelChange(a, success, highest, previous, highest); }, packet);
EXPECT_EQ(RoundTrip(packet).highestLevel, highest);
}
}
}
ClientPackets::MakeGMResponse packet;
packet.success = true;
packet.highestLevel = eGameMasterLevel::OPERATOR;
packet.previousLevel = eGameMasterLevel::CIVILIAN;
packet.newLevel = eGameMasterLevel::DEVELOPER;
RakNet::BitStream bytes;
packet.WritePacket(bytes);
EXPECT_PACKET_EQ(FromHex("53 05 00 10 00 00 00 00 01 09 00 00 00 08 00"), FromBitStream(bytes));
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, HTTPMonitorInfoAndDebugOutputMatchLegacy) {
for (int bits = 0; bits < 32; bits++) {
LegacyWorldPackets::HTTPMonitorInfo info;
info.port = static_cast<uint16_t>(80 + bits);
info.openWeb = bits & 1;
info.supportsSum = bits & 2;
info.supportsDetail = bits & 4;
info.supportsWho = bits & 8;
info.supportsObjects = bits & 16;
ClientPackets::HTTPMonitorInfoResponse packet;
packet.port = info.port;
packet.openWeb = info.openWeb;
packet.supportsSum = info.supportsSum;
packet.supportsDetail = info.supportsDetail;
packet.supportsWho = info.supportsWho;
packet.supportsObjects = info.supportsObjects;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendHTTPMonitorInfo(a, info); }, packet);
EXPECT_EQ(RoundTrip(packet).supportsWho, info.supportsWho);
}
for (const auto& text : g_Strings) {
ClientPackets::DebugOutput packet;
packet.data = text;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendDebugOuput(a, text); }, packet);
EXPECT_EQ(RoundTrip(packet).data, text);
}
}
TEST_F(WorldPacketsTests, ChatModerationStringMatchesLegacy) {
using Segments = std::set<std::pair<uint8_t, uint8_t>>;
Segments many;
for (uint8_t i = 0; i < 70; i++) many.emplace(i, static_cast<uint8_t>(i + 1));
for (const auto& segments : { Segments{}, Segments{ { 0, 4 } }, Segments{ { 2, 3 }, { 10, 255 } }, many }) {
for (const uint32_t requestID : { 0u, 7u, 255u, 256u }) {
for (const auto& receiver : g_Strings) {
for (const bool accepted : { false, true }) {
ClientPackets::ChatModerationString packet;
packet.requestAccepted = segments.empty(); // what the world server sends
packet.requestID = requestID;
packet.receiver = LUWString(receiver, 42);
packet.rejectedSegments = segments;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendChatModerationResponse(a, accepted, requestID, receiver, segments); }, packet);
if (segments.size() <= 64) EXPECT_EQ(RoundTrip(packet).rejectedSegments, segments);
}
}
}
}
}
TEST_F(WorldPacketsTests, CreateCharacterMatchesLegacy) {
for (const auto objectID : g_ObjectIDs) {
for (const auto& xml : { std::string(), std::string("<obj v=\"1\"><char acct=\"1\"/></obj>"), std::string(20000, 'x') }) {
for (const auto gm : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) {
for (const int64_t reputation : { int64_t{ 0 }, int64_t{ -5 }, std::numeric_limits<int64_t>::max() }) {
const std::u16string name = u"Some Name";
const LWOCLONEID cloneID = 1234;
ClientPackets::CreateCharacter packet;
packet.objectID = objectID;
packet.xmlData = xml;
packet.name = name;
packet.gmLevel = gm;
packet.chatMode = static_cast<int32_t>(gm);
packet.reputation = reputation;
packet.propertyCloneID = cloneID;
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCreateCharacter(a, reputation, objectID, xml, name, gm, cloneID); }, packet);
const auto copy = RoundTrip(packet);
EXPECT_EQ(copy.objectID, objectID);
EXPECT_EQ(copy.xmlData, xml);
EXPECT_EQ(copy.name, name);
EXPECT_EQ(copy.gmLevel, gm);
EXPECT_EQ(copy.reputation, reputation);
EXPECT_EQ(copy.propertyCloneID, 1234);
EXPECT_EQ(copy.templateID, 1);
}
}
}
}
ClientPackets::CreateCharacter packet;
packet.xmlData = "<obj/>";
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, CharacterListMatchesLegacy) {
FakeCharacter first;
FakeCharacter second;
second.objectID = 1152921508901814000;
second.name = "SomeName";
second.unapprovedName = std::string(40, 'u');
second.nameRejected = true;
second.shirtColor = 1; second.shirtStyle = 2; second.pantsColor = 3; second.hairStyle = 4; second.hairColor = 5;
second.leftHand = 6; second.rightHand = 7; second.eyebrows = 8; second.eyes = 9; second.mouth = 10;
second.zoneID = 1100; second.zoneInstance = 0x12345; second.zoneClone = 99;
second.lastLogin = 1790000000;
second.equippedItems = { 1, -1, 7000 };
const std::vector<std::vector<FakeCharacter*>> lists = { {}, { &first }, { &first, &second, &second, &second } };
for (const auto& list : lists) {
ClientPackets::CharacterListResponse packet;
for (const auto* character : list) {
auto& entry = packet.characters.emplace_back();
entry.objectID = character->GetObjectID();
entry.name = LUWString(character->GetName());
entry.unapprovedName = LUWString(character->GetUnapprovedName());
entry.nameRejected = character->GetNameRejected();
entry.shirtColor = character->GetShirtColor();
entry.shirtStyle = character->GetShirtStyle();
entry.pantsColor = character->GetPantsColor();
entry.hairStyle = character->GetHairStyle();
entry.hairColor = character->GetHairColor();
entry.leftHand = character->GetLeftHand();
entry.rightHand = character->GetRightHand();
entry.eyebrows = character->GetEyebrows();
entry.eyes = character->GetEyes();
entry.mouth = character->GetMouth();
entry.zoneID = character->GetZoneID();
entry.zoneInstance = character->GetZoneInstance();
entry.zoneClone = character->GetZoneClone();
entry.lastLogin = character->GetLastLogin();
entry.equippedItems = character->GetEquippedItems();
}
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterList(a, list); }, packet);
const auto copy = RoundTrip(packet);
ASSERT_EQ(copy.characters.size(), list.size());
if (list.size() > 1) {
EXPECT_EQ(copy.characters[1].name.GetAsString(), "SomeName");
EXPECT_EQ(copy.characters[1].equippedItems, second.equippedItems);
EXPECT_TRUE(copy.characters[1].nameRejected);
}
if (!list.empty()) ExpectTruncatedFails(packet);
}
}
// ---- Client -> server ----
TEST_F(WorldPacketsTests, ValidationMatchesLegacy) {
for (const auto& username : g_Strings) {
for (const auto& checksum : { std::string(), std::string("0123456789abcdef0123456789abcdef") }) {
WorldPackets::Validation packet;
packet.username = LUWString(username);
packet.sessionKey = LUWString(std::string("fdce6d87c07128819c3a13cec99de30a"));
packet.fdbChecksum = LUString(checksum, 32);
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::ReadValidation(&received.packet);
WorldPackets::Validation read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.username.GetAsString(), legacy.username);
EXPECT_EQ(read.sessionKey.GetAsString(), legacy.sessionKey);
EXPECT_EQ(read.fdbChecksum.string, legacy.checksum);
RoundTrip(packet);
ExpectTruncatedFails(packet);
}
}
// The optional trailing byte some clients send is ignored
WorldPackets::Validation packet;
packet.username = LUWString(std::string("user"));
RakNet::BitStream bytes;
packet.WritePacket(bytes);
bytes.Write<uint8_t>(0);
WorldPackets::Validation read;
ASSERT_TRUE(read.ReadHeader(bytes));
ASSERT_TRUE(read.Deserialize(bytes));
EXPECT_EQ(read.username.GetAsString(), "user");
// Golden: header then the username's first characters
RakNet::BitStream golden;
packet.WritePacket(golden);
const auto all = FromBitStream(golden);
EXPECT_PACKET_EQ(FromHex("53 04 00 01 00 00 00 00 75 00 73 00 65 00 72 00 00 00"), (PacketBytes{ { all.bytes.begin(), all.bytes.begin() + 18 }, 18 * 8 }));
EXPECT_EQ(all.bytes.size(), 8 + 66 + 66 + 32);
}
TEST_F(WorldPacketsTests, CharacterRequestsMatchLegacy) {
for (const auto objectID : g_ObjectIDs) {
WorldPackets::CharacterLoginRequest login;
login.playerID = objectID;
ReceivedPacket loginReceived(login);
WorldPackets::CharacterLoginRequest loginRead;
ASSERT_TRUE(Read(loginReceived, loginRead));
EXPECT_EQ(loginRead.playerID, LegacyWorldPackets::ReadLoginRequest(&loginReceived.packet));
ExpectTruncatedFails(login);
WorldPackets::CharacterDeleteRequest deletion;
deletion.objectID = objectID;
ReceivedPacket deleteReceived(deletion);
WorldPackets::CharacterDeleteRequest deleteRead;
ASSERT_TRUE(Read(deleteReceived, deleteRead));
EXPECT_EQ(deleteRead.objectID, LegacyWorldPackets::ReadDeleteCharacter(&deleteReceived.packet));
ExpectTruncatedFails(deletion);
for (const auto& name : g_Strings) {
WorldPackets::CharacterRenameRequest rename;
rename.objectID = objectID;
rename.name = LUWString(name);
ReceivedPacket renameReceived(rename);
WorldPackets::CharacterRenameRequest renameRead;
ASSERT_TRUE(Read(renameReceived, renameRead));
const auto [legacyID, legacyName] = LegacyWorldPackets::ReadRenameCharacter(&renameReceived.packet);
EXPECT_EQ(renameRead.objectID, legacyID);
EXPECT_EQ(renameRead.name.GetAsString(), legacyName);
RoundTrip(rename);
}
}
for (const auto& name : g_Strings) {
WorldPackets::CharacterCreateRequest create;
create.name = LUWString(name);
create.firstNameIndex = 1; create.middleNameIndex = 2; create.lastNameIndex = 0xFFFFFFFF;
create.unknown = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
create.shirtColor = 10; create.shirtStyle = 11; create.pantsColor = 12; create.hairStyle = 13; create.hairColor = 14;
create.leftHand = 15; create.rightHand = 16; create.eyebrows = 17; create.eyes = 18; create.mouth = 19;
ReceivedPacket received(create);
const auto legacy = LegacyWorldPackets::ReadCreateCharacter(&received.packet);
WorldPackets::CharacterCreateRequest read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.name.GetAsString(), legacy.LUWStringName.GetAsString());
EXPECT_EQ(read.firstNameIndex, legacy.firstNameIndex);
EXPECT_EQ(read.middleNameIndex, legacy.middleNameIndex);
EXPECT_EQ(read.lastNameIndex, legacy.lastNameIndex);
EXPECT_EQ(read.shirtColor, legacy.shirtColor);
EXPECT_EQ(read.shirtStyle, legacy.shirtStyle);
EXPECT_EQ(read.pantsColor, legacy.pantsColor);
EXPECT_EQ(read.hairStyle, legacy.hairStyle);
EXPECT_EQ(read.hairColor, legacy.hairColor);
EXPECT_EQ(read.leftHand, legacy.lh);
EXPECT_EQ(read.rightHand, legacy.rh);
EXPECT_EQ(read.eyebrows, legacy.eyebrows);
EXPECT_EQ(read.eyes, legacy.eyes);
EXPECT_EQ(read.mouth, legacy.mouth);
RoundTrip(create);
ExpectTruncatedFails(create);
}
WorldPackets::CharacterListRequest list;
RakNet::BitStream bytes;
list.WritePacket(bytes);
EXPECT_PACKET_EQ(FromHex("53 04 00 02 00 00 00 00"), FromBitStream(bytes));
}
TEST_F(WorldPacketsTests, GameMessageMatchesLegacy) {
for (const auto objectID : g_ObjectIDs) {
for (const uint32_t payloadBits : { 0u, 1u, 8u, 13u, 800u }) {
WorldPackets::GameMessage packet;
packet.objectID = objectID;
packet.messageID = MessageType::Game::REQUEST_USE;
for (uint32_t i = 0; i < payloadBits; i++) packet.data.Write(i % 3 == 0);
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::ReadGameMessage(&received.packet);
WorldPackets::GameMessage read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.objectID, legacy.objectID);
EXPECT_EQ(read.messageID, legacy.messageID);
// A packet is whole bytes, so the data read is the payload padded to a byte, as before
EXPECT_EQ(read.data.GetNumberOfBitsUsed(), legacy.dataBits);
EXPECT_PACKET_EQ((PacketBytes{ legacy.data, legacy.dataBits }), FromBitStream(read.data));
}
}
WorldPackets::GameMessage packet;
packet.objectID = 0x0102030405060708;
packet.messageID = static_cast<MessageType::Game>(0x0A0B);
RakNet::BitStream bytes;
packet.WritePacket(bytes);
EXPECT_PACKET_EQ(FromHex("53 04 00 05 00 00 00 00 08 07 06 05 04 03 02 01 0b 0a"), FromBitStream(bytes));
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, PositionUpdateMatchesLegacy) {
for (int flags = 0; flags < 32; flags++) {
WorldPackets::PositionUpdate packet;
auto& update = packet.update;
update.position = NiPoint3(1.0f, -2.0f, 3.5f);
update.rotation = NiQuaternion(0.5f, 0.1f, 0.2f, 0.3f);
update.onGround = flags & 1;
update.onRail = !(flags & 1);
packet.hasVelocity = flags & 2;
update.velocity = packet.hasVelocity ? NiPoint3(4.0f, 5.0f, 6.0f) : NiPoint3Constant::ZERO;
packet.hasAngularVelocity = flags & 4;
update.angularVelocity = packet.hasAngularVelocity ? NiPoint3(7.0f, 8.0f, 9.0f) : NiPoint3Constant::ZERO;
packet.hasLocalSpaceInfo = flags & 8;
packet.hasLinearVelocity = packet.hasLocalSpaceInfo && (flags & 1);
if (packet.hasLocalSpaceInfo) {
update.localSpaceInfo.objectId = 1234;
update.localSpaceInfo.position = NiPoint3(10.0f, 11.0f, 12.0f);
if (packet.hasLinearVelocity) update.localSpaceInfo.linearVelocity = NiPoint3(13.0f, 14.0f, 15.0f);
}
packet.hasRemoteInputInfo = flags & 16;
if (packet.hasRemoteInputInfo) {
update.remoteInputInfo.m_RemoteInputX = 0.25f;
update.remoteInputInfo.m_RemoteInputY = -0.75f;
update.remoteInputInfo.m_IsPowersliding = true;
update.remoteInputInfo.m_IsModified = flags & 1;
}
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::HandleClientPositionUpdate(&received.packet);
WorldPackets::PositionUpdate read;
ASSERT_TRUE(Read(received, read));
const auto& got = read.update;
EXPECT_EQ(got.position, legacy.position);
EXPECT_EQ(got.rotation, legacy.rotation);
EXPECT_EQ(got.onGround, legacy.onGround);
EXPECT_EQ(got.onRail, legacy.onRail);
EXPECT_EQ(got.velocity, legacy.velocity);
EXPECT_EQ(got.angularVelocity, legacy.angularVelocity);
EXPECT_EQ(got.localSpaceInfo.objectId, legacy.localSpaceInfo.objectId);
EXPECT_EQ(got.localSpaceInfo.position, legacy.localSpaceInfo.position);
EXPECT_EQ(got.localSpaceInfo.linearVelocity, legacy.localSpaceInfo.linearVelocity);
auto remote = got.remoteInputInfo;
EXPECT_TRUE(remote == legacy.remoteInputInfo);
RoundTrip(packet);
}
// Like before, the last two sections may be left out entirely
WorldPackets::PositionUpdate packet;
RakNet::BitStream bytes;
packet.WritePacket(bytes);
RakNet::BitStream shortened(bytes.GetData(), bytes.GetNumberOfBytesUsed(), true);
shortened.SetWriteOffset(bytes.GetNumberOfBitsUsed() - 2); // drop the two trailing flags
WorldPackets::PositionUpdate read;
ASSERT_TRUE(read.ReadHeader(shortened));
EXPECT_TRUE(read.Deserialize(shortened));
EXPECT_FALSE(read.hasLocalSpaceInfo);
EXPECT_FALSE(read.hasRemoteInputInfo);
}
TEST_F(WorldPacketsTests, StringCheckMatchesLegacy) {
const std::vector<std::u16string> receivers = { u"", u"Bob", u"[GM]Mythran", u"[GM]", std::u16string(42, u'r') };
const std::vector<std::u16string> messages = { u"", u"hi", u"hello there", std::u16string(300, u'm') };
for (const auto& receiver : receivers) {
for (const auto& message : messages) {
for (const uint8_t chatLevel : { uint8_t{ 0 }, uint8_t{ 1 } }) {
WorldPackets::StringCheck packet;
packet.chatLevel = chatLevel;
packet.requestID = 0xa2;
packet.receiver = receiver;
packet.receiver.resize(42);
// what clients leave after the name
if (receiver.size() < 40) packet.receiver[receiver.size() + 1] = u'\x1894';
packet.message = message;
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::HandleChatModerationRequest(&received.packet);
WorldPackets::StringCheck read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.chatLevel, legacy.chatLevel);
EXPECT_EQ(read.requestID, legacy.requestID);
EXPECT_EQ(read.GetNarrowReceiver(), legacy.receiver);
EXPECT_EQ(read.GetNarrowMessage(), legacy.message);
RoundTrip(packet);
ExpectTruncatedFails(packet);
}
}
}
}
TEST_F(WorldPacketsTests, GeneralChatMessageMatchesLegacy) {
for (const auto& message : { std::u16string(), std::u16string(u"how long are you going to stay there?"), std::u16string(2000, u'x') }) {
for (const uint8_t channel : { uint8_t{ 0 }, uint8_t{ 4 } }) {
WorldPackets::GeneralChatMessage packet;
packet.chatChannel = channel;
packet.unknown = 0x0900;
packet.message = message;
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::HandleChatMessage(&received.packet);
WorldPackets::GeneralChatMessage read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.chatChannel, legacy.chatChannel);
EXPECT_EQ(read.unknown, legacy.unknown);
EXPECT_EQ(read.message, legacy.message);
RoundTrip(packet);
}
}
WorldPackets::GeneralChatMessage packet;
packet.chatChannel = 4;
packet.message = u"hi";
RakNet::BitStream bytes;
packet.WritePacket(bytes);
// channel | unknown u16 | count 3 (includes the terminator) | "hi" | terminator
EXPECT_PACKET_EQ(FromHex("53 04 00 0e 00 00 00 00 04 00 00 03 00 00 00 68 00 69 00 00 00"), FromBitStream(bytes));
// A negative or huge count is dropped
for (const int32_t count : { -1, static_cast<int32_t>(MAX_MESSAGE_LENGTH) + 1 }) {
RakNet::BitStream bad;
LUBitStream(ServiceType::WORLD, MessageType::World::GENERAL_CHAT_MESSAGE).WriteHeader(bad);
bad.Write<uint8_t>(0);
bad.Write<uint16_t>(0);
bad.Write(count);
WorldPackets::GeneralChatMessage read;
ASSERT_TRUE(read.ReadHeader(bad));
EXPECT_FALSE(read.Deserialize(bad));
}
}
TEST_F(WorldPacketsTests, RoutePacketMatchesLegacy) {
for (const uint32_t bodySize : { 0u, 1u, 4u, 20u }) {
for (const uint32_t declaredSize : { 0u, 2u, 11u, 100u, 20001u }) {
for (const LWOOBJID sender : { LWOOBJID{ 0 }, LWOOBJID{ 1152921508901814000 } }) {
WorldPackets::RoutePacket packet;
packet.size = declaredSize;
packet.routedService = ServiceType::CHAT;
packet.routedMessageID = 0x10A;
for (uint32_t i = 0; i < bodySize; i++) packet.routedData.push_back(static_cast<uint8_t>(i + 1));
ReceivedPacket received(packet);
RakNet::BitStream legacy;
const bool legacyForwarded = LegacyWorldPackets::BuildRoutePacket(&received.packet, sender, legacy);
WorldPackets::RoutePacket read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.routedData, packet.routedData);
EXPECT_EQ(legacyForwarded, read.size <= 20000); // the world server's size check
if (!legacyForwarded) continue;
RakNet::BitStream converted;
read.ToChat(sender).WritePacket(converted);
EXPECT_PACKET_EQ(FromBitStream(legacy), FromBitStream(converted));
RoundTrip(packet);
}
}
}
WorldPackets::RoutePacket packet;
ExpectTruncatedFails(packet);
}
TEST_F(WorldPacketsTests, SmallRequestsMatchLegacy) {
for (const auto type : magic_enum::enum_values<eFunnessTypes>()) {
for (const float info : { 0.0f, 1.0f, 6.5f, -3.0f }) {
WorldPackets::HandleFunness packet;
packet.cheatType = type;
packet.cheatInfo = info;
ReceivedPacket received(packet);
const auto legacy = LegacyWorldPackets::ReadFunness(&received.packet);
WorldPackets::HandleFunness read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.cheatType, legacy.cheatType);
EXPECT_EQ(read.cheatInfo, legacy.cheatInfo);
ExpectTruncatedFails(packet);
}
}
for (const int32_t language : { 0, 1, 2, 3, -1 }) {
WorldPackets::UIHelpTop5 packet;
packet.language = language;
ReceivedPacket received(packet);
WorldPackets::UIHelpTop5 read;
ASSERT_TRUE(Read(received, read));
EXPECT_EQ(read.language, LegacyWorldPackets::SendTop5HelpIssues(&received.packet));
ExpectTruncatedFails(packet);
}
WorldPackets::LevelLoadComplete level;
level.mapID = 1101;
level.instanceID = 46671;
RakNet::BitStream bytes;
level.WritePacket(bytes);
EXPECT_PACKET_EQ(FromHex("53 04 00 13 00 00 00 00 4d 04 4f b6 00 00 00 00"), FromBitStream(bytes));
EXPECT_EQ(RoundTrip(level).instanceID, 46671);
ExpectTruncatedFails(level);
WorldPackets::MailPacket mail;
mail.data.Write<uint32_t>(0x01020304);
RakNet::BitStream mailBytes;
mail.WritePacket(mailBytes);
WorldPackets::MailPacket mailRead;
ASSERT_TRUE(mailRead.ReadHeader(mailBytes));
ASSERT_TRUE(mailRead.Deserialize(mailBytes));
EXPECT_PACKET_EQ(FromBitStream(mail.data), FromBitStream(mailRead.data));
}