Friends List Overhaul (#630) v103

* Add friends list migration

* Change friends to use charID

Update friends table to use charID and not LWOOBJID variant.

* Fix remove friend

Fix remove friend and make the query more readable at a glance.

* Add and remove friends in the container

Properly add and remove friends in the player container

* add enums

* Add best friends and basic GM support V1

* Add more features

* not online / doesnt exist implementation

Implements the not online and invalid character response codes

* Address players not being removed

Fix an issue where players would not be marked as offline in the friends list due to the message not being sent in all circumstances.

Tested changes on 3 clients, switching characters, logging out from character select, switching characters, world transfer and my friends list looked as it was supposed to.

* Implement proper friends system

Remove debug logs

Track count of best friends

Add best friends list cap of 5

Add config option and best friend update

Add a config option and implement the last missing best friend serialization

Added comments and fixed remove best friend bug

Added some comments and addressed an issue where removing best friends would not remove them from your internal count of friends.

properties and logs fixes

whoops, had an issue

send reply if already BFFs

Send the correct objectID

I really need to rename these

Fix white space

goon

* Replace queries with unique ptrs

* remove user from player container on deletion

Remove the user from the player container when they delete their character.
This commit is contained in:
David Markowitz 2022-07-12 20:36:06 -07:00 committed by GitHub
parent 5888c9f468
commit 24dbd3944d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 321 additions and 109 deletions

View File

@ -1,6 +1,6 @@
PROJECT_VERSION_MAJOR=1 PROJECT_VERSION_MAJOR=1
PROJECT_VERSION_MINOR=0 PROJECT_VERSION_MINOR=0
PROJECT_VERSION_PATCH=2 PROJECT_VERSION_PATCH=3
# LICENSE # LICENSE
LICENSE=AGPL-3.0 LICENSE=AGPL-3.0
# The network version. # The network version.

View File

@ -8,6 +8,10 @@
#include "dServer.h" #include "dServer.h"
#include "GeneralUtils.h" #include "GeneralUtils.h"
#include "dLogger.h" #include "dLogger.h"
#include "AddFriendResponseCode.h"
#include "AddFriendResponseType.h"
#include "RakString.h"
#include "dConfig.h"
extern PlayerContainer playerContainer; extern PlayerContainer playerContainer;
@ -22,20 +26,20 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
if (!player) return; if (!player) return;
//Get our friends list from the Db. Using a derived table since the friend of a player can be in either column. //Get our friends list from the Db. Using a derived table since the friend of a player can be in either column.
auto stmt = Database::CreatePreppedStmt( std::unique_ptr<sql::PreparedStatement> stmt(Database::CreatePreppedStmt(
"SELECT fr.requested_player, best_friend, ci.name FROM " "SELECT fr.requested_player, best_friend, ci.name FROM "
"(SELECT CASE " "(SELECT CASE "
"WHEN player_id = ? THEN friend_id " "WHEN player_id = ? THEN friend_id "
"WHEN friend_id = ? THEN player_id " "WHEN friend_id = ? THEN player_id "
"END AS requested_player, best_friend FROM friends) AS fr " "END AS requested_player, best_friend FROM friends) AS fr "
"JOIN charinfo AS ci ON ci.id = fr.requested_player " "JOIN charinfo AS ci ON ci.id = fr.requested_player "
"WHERE fr.requested_player IS NOT NULL;"); "WHERE fr.requested_player IS NOT NULL;"));
stmt->setUInt(1, static_cast<uint32_t>(playerID)); stmt->setUInt(1, static_cast<uint32_t>(playerID));
stmt->setUInt(2, static_cast<uint32_t>(playerID)); stmt->setUInt(2, static_cast<uint32_t>(playerID));
std::vector<FriendData> friends; std::vector<FriendData> friends;
auto res = stmt->executeQuery(); std::unique_ptr<sql::ResultSet> res(stmt->executeQuery());
while (res->next()) { while (res->next()) {
FriendData fd; FriendData fd;
fd.isFTP = false; // not a thing in DLU fd.isFTP = false; // not a thing in DLU
@ -43,18 +47,19 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
GeneralUtils::SetBit(fd.friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT)); GeneralUtils::SetBit(fd.friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT));
GeneralUtils::SetBit(fd.friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER)); GeneralUtils::SetBit(fd.friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER));
fd.isBestFriend = res->getInt(2) == 2; //0 = friends, 1 = requested, 2 = bffs fd.isBestFriend = res->getInt(2) == 3; //0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
if (fd.isBestFriend) player->countOfBestFriends+=1;
fd.friendName = res->getString(3); fd.friendName = res->getString(3);
//Now check if they're online: //Now check if they're online:
auto fr = playerContainer.GetPlayerData(fd.friendID); auto fr = playerContainer.GetPlayerData(fd.friendID);
Game::logger->Log("ChatPacketHandler", "friend is %llu\n", fd.friendID);
if (fr) { if (fr) {
fd.isOnline = true; fd.isOnline = true;
fd.zoneID = fr->zoneID; fd.zoneID = fr->zoneID;
//Since this friend is online, we need to update them on the fact that we've just logged in: //Since this friend is online, we need to update them on the fact that we've just logged in:
SendFriendUpdate(fr, player, 1); SendFriendUpdate(fr, player, 1, fd.isBestFriend);
} }
else { else {
fd.isOnline = false; fd.isOnline = false;
@ -64,11 +69,6 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
friends.push_back(fd); friends.push_back(fd);
} }
delete res;
res = nullptr;
delete stmt;
stmt = nullptr;
//Now, we need to send the friendlist to the server they came from: //Now, we need to send the friendlist to the server they came from:
CBITSTREAM; CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER); PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER);
@ -91,18 +91,150 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
} }
void ChatPacketHandler::HandleFriendRequest(Packet* packet) { void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
auto maxNumberOfBestFriendsAsString = Game::config->GetValue("max_number_of_best_friends");
// If this config option doesn't exist, default to 5 which is what live used.
auto maxNumberOfBestFriends = maxNumberOfBestFriendsAsString != "" ? std::stoi(maxNumberOfBestFriendsAsString) : 5U;
CINSTREAM; CINSTREAM;
LWOOBJID playerID; LWOOBJID requestorPlayerID;
inStream.Read(playerID); inStream.Read(requestorPlayerID);
inStream.Read(playerID); inStream.Read(requestorPlayerID);
std::string playerName = PacketUtils::ReadString(0x14, packet, true); uint32_t spacing{};
//There's another bool here to determine if it's a best friend request, but we're not handling it right now. inStream.Read(spacing);
std::string playerName = "";
uint16_t character;
bool noMoreLettersInName = false;
//We need to check to see if the player is actually online or not: for (uint32_t j = 0; j < 33; j++) {
auto targetData = playerContainer.GetPlayerData(playerName); inStream.Read(character);
if (targetData) { if (character == '\0') noMoreLettersInName = true;
SendFriendRequest(targetData, playerContainer.GetPlayerData(playerID)); if (!noMoreLettersInName) playerName.push_back(static_cast<char>(character));
} }
char isBestFriendRequest{};
inStream.Read(isBestFriendRequest);
auto requestor = playerContainer.GetPlayerData(requestorPlayerID);
std::unique_ptr<PlayerData> requestee(playerContainer.GetPlayerData(playerName));
// Check if player is online first
if (isBestFriendRequest && !requestee) {
for (auto friendDataCandidate : requestor->friends) {
if (friendDataCandidate.friendName == playerName) {
requestee.reset(new PlayerData());
// Setup the needed info since you can add a best friend offline.
requestee->playerID = friendDataCandidate.friendID;
requestee->playerName = RakNet::RakString(friendDataCandidate.friendName.c_str());
requestee->zoneID = LWOZONEID();
FriendData requesteeFriendData{};
requesteeFriendData.friendID = requestor->playerID;
requesteeFriendData.friendName = requestor->playerName;
requesteeFriendData.isFTP = false;
requesteeFriendData.isOnline = false;
requesteeFriendData.zoneID = requestor->zoneID;
requestee->friends.push_back(requesteeFriendData);
requestee->sysAddr = UNASSIGNED_SYSTEM_ADDRESS;
break;
}
}
}
// If at this point we dont have a target, then they arent online and we cant send the request.
// Send the response code that corresponds to what the error is.
if (!requestee) {
std::unique_ptr<sql::PreparedStatement> nameQuery(Database::CreatePreppedStmt("SELECT name from charinfo where name = ?;"));
nameQuery->setString(1, playerName);
std::unique_ptr<sql::ResultSet> result(nameQuery->executeQuery());
requestee.reset(new PlayerData());
requestee->playerName = RakNet::RakString(playerName.c_str());
SendFriendResponse(requestor, requestee.get(), result->next() ? AddFriendResponseType::NOTONLINE : AddFriendResponseType::INVALIDCHARACTER);
return;
}
if (isBestFriendRequest) {
std::unique_ptr<sql::PreparedStatement> friendUpdate(Database::CreatePreppedStmt("SELECT * FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;"));
friendUpdate->setUInt(1, static_cast<uint32_t>(requestorPlayerID));
friendUpdate->setUInt(2, static_cast<uint32_t>(requestee->playerID));
friendUpdate->setUInt(3, static_cast<uint32_t>(requestee->playerID));
friendUpdate->setUInt(4, static_cast<uint32_t>(requestorPlayerID));
std::unique_ptr<sql::ResultSet> result(friendUpdate->executeQuery());
LWOOBJID queryPlayerID = LWOOBJID_EMPTY;
LWOOBJID queryFriendID = LWOOBJID_EMPTY;
uint8_t oldBestFriendStatus{};
uint8_t bestFriendStatus{};
if (result->next()) {
// Get the IDs
queryPlayerID = result->getInt(1);
queryFriendID = result->getInt(2);
oldBestFriendStatus = result->getInt(3);
bestFriendStatus = oldBestFriendStatus;
// Set the bits
GeneralUtils::SetBit(queryPlayerID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER));
GeneralUtils::SetBit(queryPlayerID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT));
GeneralUtils::SetBit(queryFriendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER));
GeneralUtils::SetBit(queryFriendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT));
// Since this player can either be the friend of someone else or be friends with someone else
// their column in the database determines what bit gets set. When the value hits 3, they
// are now best friends with the other player.
if (queryPlayerID == requestorPlayerID) {
bestFriendStatus |= 1ULL << 0;
} else {
bestFriendStatus |= 1ULL << 1;
}
}
// Only do updates if there was a change in the bff status.
if (oldBestFriendStatus != bestFriendStatus) {
if (requestee->countOfBestFriends >= maxNumberOfBestFriends || requestor->countOfBestFriends >= maxNumberOfBestFriends) {
if (requestee->countOfBestFriends >= maxNumberOfBestFriends) {
SendFriendResponse(requestor, requestee.get(), AddFriendResponseType::THEIRFRIENDLISTFULL, false);
}
if (requestor->countOfBestFriends >= maxNumberOfBestFriends) {
SendFriendResponse(requestor, requestee.get(), AddFriendResponseType::YOURFRIENDSLISTFULL, false);
}
} else {
// Then update the database with this new info.
std::unique_ptr<sql::PreparedStatement> updateQuery(Database::CreatePreppedStmt("UPDATE friends SET best_friend = ? WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;"));
updateQuery->setUInt(1, bestFriendStatus);
updateQuery->setUInt(2, static_cast<uint32_t>(requestorPlayerID));
updateQuery->setUInt(3, static_cast<uint32_t>(requestee->playerID));
updateQuery->setUInt(4, static_cast<uint32_t>(requestee->playerID));
updateQuery->setUInt(5, static_cast<uint32_t>(requestorPlayerID));
updateQuery->executeUpdate();
// Sent the best friend update here if the value is 3
if (bestFriendStatus == 3U) {
requestee->countOfBestFriends+=1;
requestor->countOfBestFriends+=1;
if (requestee->sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee.get(), requestor, AddFriendResponseType::ACCEPTED, false, true);
if (requestor->sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee.get(), AddFriendResponseType::ACCEPTED, false, true);
for (auto& friendData : requestor->friends) {
if (friendData.friendID == requestee->playerID) {
friendData.isBestFriend = true;
}
}
for (auto& friendData : requestee->friends) {
if (friendData.friendID == requestor->playerID) {
friendData.isBestFriend = true;
}
}
}
}
} else {
if (requestor->sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee.get(), AddFriendResponseType::WAITINGAPPROVAL, true, true);
}
} else {
// Do not send this if we are requesting to be a best friend.
SendFriendRequest(requestee.get(), requestor);
}
// If the player is actually a player and not a ghost one defined above, release it from being deleted.
if (requestee->sysAddr != UNASSIGNED_SYSTEM_ADDRESS) requestee.release();
} }
void ChatPacketHandler::HandleFriendResponse(Packet* packet) { void ChatPacketHandler::HandleFriendResponse(Packet* packet) {
@ -111,52 +243,74 @@ void ChatPacketHandler::HandleFriendResponse(Packet* packet) {
inStream.Read(playerID); inStream.Read(playerID);
inStream.Read(playerID); inStream.Read(playerID);
uint8_t responseCode = packet->data[0x14]; AddFriendResponseCode clientResponseCode = static_cast<AddFriendResponseCode>(packet->data[0x14]);
std::string friendName = PacketUtils::ReadString(0x15, packet, true); std::string friendName = PacketUtils::ReadString(0x15, packet, true);
//Now to try and find both of these: //Now to try and find both of these:
auto goonA = playerContainer.GetPlayerData(playerID); auto requestor = playerContainer.GetPlayerData(playerID);
auto goonB = playerContainer.GetPlayerData(friendName); auto requestee = playerContainer.GetPlayerData(friendName);
if (!goonA || !goonB) return; if (!requestor || !requestee) return;
if (responseCode != 0) return; //If we're not accepting the request, end here, do not insert to friends table. AddFriendResponseType serverResponseCode{};
uint8_t isAlreadyBestFriends = 0U;
for (auto friendData : goonA->friends) { // We need to convert this response code to one we can actually send back to the client.
if (friendData.friendID == goonB->playerID) return; switch (clientResponseCode) {
case AddFriendResponseCode::ACCEPTED:
serverResponseCode = AddFriendResponseType::ACCEPTED;
break;
case AddFriendResponseCode::BUSY:
serverResponseCode = AddFriendResponseType::BUSY;
break;
case AddFriendResponseCode::CANCELLED:
serverResponseCode = AddFriendResponseType::CANCELLED;
break;
case AddFriendResponseCode::REJECTED:
serverResponseCode = AddFriendResponseType::DECLINED;
break;
} }
for (auto friendData : goonB->friends) { // Now that we have handled the base cases, we need to check the other cases.
if (friendData.friendID == goonA->playerID) return; if (serverResponseCode == AddFriendResponseType::ACCEPTED) {
for (auto friendData : requestor->friends) {
if (friendData.friendID == requestee->playerID) {
serverResponseCode = AddFriendResponseType::ALREADYFRIEND;
if (friendData.isBestFriend) {
isAlreadyBestFriends = 1U;
}
}
}
} }
// Add the goons to their friends list // This message is NOT sent for best friends and is handled differently for those requests.
FriendData goonAData; if (serverResponseCode == AddFriendResponseType::ACCEPTED) {
goonAData.zoneID = goonA->zoneID; // Add the each player to the others friend list.
goonAData.friendID = goonA->playerID; FriendData requestorData;
goonAData.friendName = goonA->playerName; requestorData.zoneID = requestor->zoneID;
goonAData.isBestFriend = false; requestorData.friendID = requestor->playerID;
goonAData.isFTP = false; requestorData.friendName = requestor->playerName;
goonAData.isOnline = true; requestorData.isBestFriend = false;
goonB->friends.push_back(goonAData); requestorData.isFTP = false;
requestorData.isOnline = true;
requestee->friends.push_back(requestorData);
FriendData goonBData; FriendData requesteeData;
goonBData.zoneID = goonB->zoneID; requesteeData.zoneID = requestee->zoneID;
goonBData.friendID = goonB->playerID; requesteeData.friendID = requestee->playerID;
goonBData.friendName = goonB->playerName; requesteeData.friendName = requestee->playerName;
goonBData.isBestFriend = false; requesteeData.isBestFriend = false;
goonBData.isFTP = false; requesteeData.isFTP = false;
goonBData.isOnline = true; requesteeData.isOnline = true;
goonA->friends.push_back(goonBData); requestor->friends.push_back(requesteeData);
SendFriendResponse(goonB, goonA, responseCode); std::unique_ptr<sql::PreparedStatement> statement(Database::CreatePreppedStmt("INSERT IGNORE INTO `friends` (`player_id`, `friend_id`, `best_friend`) VALUES (?,?,?);"));
SendFriendResponse(goonA, goonB, responseCode); //Do we need to send it to both? I think so so both get the updated friendlist but... idk. statement->setUInt(1, static_cast<uint32_t>(requestor->playerID));
statement->setUInt(2, static_cast<uint32_t>(requestee->playerID));
statement->setInt(3, 0);
statement->execute();
}
auto stmt = Database::CreatePreppedStmt("INSERT IGNORE INTO `friends` (`player_id`, `friend_id`, `best_friend`) VALUES (?,?,?)"); if (serverResponseCode != AddFriendResponseType::DECLINED) SendFriendResponse(requestor, requestee, serverResponseCode, isAlreadyBestFriends);
stmt->setUInt(1, static_cast<uint32_t>(goonA->playerID)); if (serverResponseCode != AddFriendResponseType::ALREADYFRIEND) SendFriendResponse(requestee, requestor, serverResponseCode, isAlreadyBestFriends);
stmt->setUInt(2, static_cast<uint32_t>(goonB->playerID));
stmt->setInt(3, 0);
stmt->execute();
delete stmt;
} }
void ChatPacketHandler::HandleRemoveFriend(Packet* packet) { void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
@ -168,11 +322,11 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
//we'll have to query the db here to find the user, since you can delete them while they're offline. //we'll have to query the db here to find the user, since you can delete them while they're offline.
//First, we need to find their ID: //First, we need to find their ID:
auto stmt = Database::CreatePreppedStmt("SELECT id FROM charinfo WHERE name=? LIMIT 1;"); std::unique_ptr<sql::PreparedStatement> stmt(Database::CreatePreppedStmt("SELECT id FROM charinfo WHERE name=? LIMIT 1;"));
stmt->setString(1, friendName.c_str()); stmt->setString(1, friendName.c_str());
LWOOBJID friendID = 0; LWOOBJID friendID = 0;
auto res = stmt->executeQuery(); std::unique_ptr<sql::ResultSet> res(stmt->executeQuery());
while (res->next()) { while (res->next()) {
friendID = res->getUInt(1); friendID = res->getUInt(1);
} }
@ -181,27 +335,20 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
GeneralUtils::SetBit(friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT)); GeneralUtils::SetBit(friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_PERSISTENT));
GeneralUtils::SetBit(friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER)); GeneralUtils::SetBit(friendID, static_cast<size_t>(eObjectBits::OBJECT_BIT_CHARACTER));
delete res; std::unique_ptr<sql::PreparedStatement> deletestmt(Database::CreatePreppedStmt("DELETE FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;"));
res = nullptr;
delete stmt;
stmt = nullptr;
auto deletestmt = Database::CreatePreppedStmt("DELETE FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;");
deletestmt->setUInt(1, static_cast<uint32_t>(playerID)); deletestmt->setUInt(1, static_cast<uint32_t>(playerID));
deletestmt->setUInt(2, static_cast<uint32_t>(friendID)); deletestmt->setUInt(2, static_cast<uint32_t>(friendID));
deletestmt->setUInt(3, static_cast<uint32_t>(friendID)); deletestmt->setUInt(3, static_cast<uint32_t>(friendID));
deletestmt->setUInt(4, static_cast<uint32_t>(playerID)); deletestmt->setUInt(4, static_cast<uint32_t>(playerID));
deletestmt->execute(); deletestmt->execute();
delete deletestmt;
deletestmt = nullptr;
//Now, we need to send an update to notify the sender (and possibly, receiver) that their friendship has been ended: //Now, we need to send an update to notify the sender (and possibly, receiver) that their friendship has been ended:
auto goonA = playerContainer.GetPlayerData(playerID); auto goonA = playerContainer.GetPlayerData(playerID);
if (goonA) { if (goonA) {
// Remove the friend from our list of friends // Remove the friend from our list of friends
for (auto friendData = goonA->friends.begin(); friendData != goonA->friends.end(); friendData++) { for (auto friendData = goonA->friends.begin(); friendData != goonA->friends.end(); friendData++) {
if ((*friendData).friendID == friendID) { if ((*friendData).friendID == friendID) {
if ((*friendData).isBestFriend) --goonA->countOfBestFriends;
goonA->friends.erase(friendData); goonA->friends.erase(friendData);
break; break;
} }
@ -214,6 +361,7 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
// Do it again for other person // Do it again for other person
for (auto friendData = goonB->friends.begin(); friendData != goonB->friends.end(); friendData++) { for (auto friendData = goonB->friends.begin(); friendData != goonB->friends.end(); friendData++) {
if ((*friendData).friendID == playerID) { if ((*friendData).friendID == playerID) {
if ((*friendData).isBestFriend) --goonB->countOfBestFriends;
goonB->friends.erase(friendData); goonB->friends.erase(friendData);
break; break;
} }
@ -766,7 +914,7 @@ void ChatPacketHandler::SendTeamSetOffWorldFlag(PlayerData* receiver, LWOOBJID i
SEND_PACKET; SEND_PACKET;
} }
void ChatPacketHandler::SendFriendUpdate(PlayerData* friendData, PlayerData* playerData, uint8_t notifyType) { void ChatPacketHandler::SendFriendUpdate(PlayerData* friendData, PlayerData* playerData, uint8_t notifyType, uint8_t isBestFriend) {
/*chat notification is displayed if log in / out and friend is updated in friends list /*chat notification is displayed if log in / out and friend is updated in friends list
[u8] - update type [u8] - update type
Update types Update types
@ -804,19 +952,20 @@ void ChatPacketHandler::SendFriendUpdate(PlayerData* friendData, PlayerData* pla
bitStream.Write(playerData->zoneID.GetCloneID()); bitStream.Write(playerData->zoneID.GetCloneID());
} }
bitStream.Write<uint8_t>(0); //isBFF bitStream.Write<uint8_t>(isBestFriend); //isBFF
bitStream.Write<uint8_t>(0); //isFTP bitStream.Write<uint8_t>(0); //isFTP
SystemAddress sysAddr = friendData->sysAddr; SystemAddress sysAddr = friendData->sysAddr;
SEND_PACKET; SEND_PACKET;
} }
void ChatPacketHandler::SendFriendRequest(PlayerData* receiver, PlayerData* sender, bool isBFFReq) { void ChatPacketHandler::SendFriendRequest(PlayerData* receiver, PlayerData* sender) {
if (!receiver || !sender) return; if (!receiver || !sender) return;
//Make sure people aren't requesting people that they're already friends with: //Make sure people aren't requesting people that they're already friends with:
for (auto fr : receiver->friends) { for (auto fr : receiver->friends) {
if (fr.friendID == sender->playerID) { if (fr.friendID == sender->playerID) {
SendFriendResponse(sender, receiver, AddFriendResponseType::ALREADYFRIEND, fr.isBestFriend);
return; //we have this player as a friend, yeet this function so it doesn't send another request. return; //we have this player as a friend, yeet this function so it doesn't send another request.
} }
} }
@ -828,29 +977,33 @@ void ChatPacketHandler::SendFriendRequest(PlayerData* receiver, PlayerData* send
//portion that will get routed: //portion that will get routed:
PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_ADD_FRIEND_REQUEST); PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_ADD_FRIEND_REQUEST);
PacketUtils::WritePacketWString(sender->playerName.C_String(), 33, &bitStream); PacketUtils::WritePacketWString(sender->playerName.C_String(), 33, &bitStream);
bitStream.Write<uint8_t>(0); bitStream.Write<uint8_t>(0); // This is a BFF flag however this is unused in live and does not have an implementation client side.
SystemAddress sysAddr = receiver->sysAddr; SystemAddress sysAddr = receiver->sysAddr;
SEND_PACKET; SEND_PACKET;
} }
void ChatPacketHandler::SendFriendResponse(PlayerData* receiver, PlayerData* sender, uint8_t responseCode) { void ChatPacketHandler::SendFriendResponse(PlayerData* receiver, PlayerData* sender, AddFriendResponseType responseCode, uint8_t isBestFriendsAlready, uint8_t isBestFriendRequest) {
if (!receiver || !sender) return; if (!receiver || !sender) return;
CBITSTREAM; CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER); PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER);
bitStream.Write(receiver->playerID); bitStream.Write(receiver->playerID);
//portion that will get routed: // Portion that will get routed:
PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_ADD_FRIEND_RESPONSE); PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_ADD_FRIEND_RESPONSE);
bitStream.Write<uint8_t>(responseCode); bitStream.Write(responseCode);
bitStream.Write<uint8_t>(1); //isOnline // For all requests besides accepted, write a flag that says whether or not we are already best friends with the receiver.
bitStream.Write<uint8_t>(responseCode != AddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender->sysAddr != UNASSIGNED_SYSTEM_ADDRESS);
// Then write the player name
PacketUtils::WritePacketWString(sender->playerName.C_String(), 33, &bitStream); PacketUtils::WritePacketWString(sender->playerName.C_String(), 33, &bitStream);
bitStream.Write(sender->playerID); // Then if this is an acceptance code, write the following extra info.
bitStream.Write(sender->zoneID); if (responseCode == AddFriendResponseType::ACCEPTED) {
bitStream.Write<uint8_t>(0); //isBFF bitStream.Write(sender->playerID);
bitStream.Write<uint8_t>(0); //isFTP bitStream.Write(sender->zoneID);
bitStream.Write(isBestFriendRequest); //isBFF
bitStream.Write<uint8_t>(0); //isFTP
}
SystemAddress sysAddr = receiver->sysAddr; SystemAddress sysAddr = receiver->sysAddr;
SEND_PACKET; SEND_PACKET;
} }

View File

@ -4,6 +4,7 @@
#include "BitStream.h" #include "BitStream.h"
struct PlayerData; struct PlayerData;
enum class AddFriendResponseType : uint8_t;
namespace ChatPacketHandler { namespace ChatPacketHandler {
void HandleFriendlistRequest(Packet* packet); void HandleFriendlistRequest(Packet* packet);
@ -31,10 +32,9 @@ namespace ChatPacketHandler {
void SendTeamSetOffWorldFlag(PlayerData* receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); void SendTeamSetOffWorldFlag(PlayerData* receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID);
//FriendData is the player we're SENDING this stuff to. Player is the friend that changed state. //FriendData is the player we're SENDING this stuff to. Player is the friend that changed state.
void SendFriendUpdate(PlayerData* friendData, PlayerData* playerData, uint8_t notifyType); void SendFriendUpdate(PlayerData* friendData, PlayerData* playerData, uint8_t notifyType, uint8_t isBestFriend);
void SendFriendRequest(PlayerData* receiver, PlayerData* sender, bool isBFFReq = false); void SendFriendRequest(PlayerData* receiver, PlayerData* sender);
void SendFriendResponse(PlayerData* receiver, PlayerData* sender, uint8_t responseCode = 3); void SendFriendResponse(PlayerData* receiver, PlayerData* sender, AddFriendResponseType responseCode, uint8_t isBestFriendsAlready = 0U, uint8_t isBestFriendRequest = 0U);
void SendRemoveFriend(PlayerData* receiver, std::string& personToRemove, bool isSuccessful); void SendRemoveFriend(PlayerData* receiver, std::string& personToRemove, bool isSuccessful);
}; };

View File

@ -59,7 +59,7 @@ void PlayerContainer::RemovePlayer(Packet* packet) {
//if (!fr.isOnline) continue; //if (!fr.isOnline) continue;
auto fd = this->GetPlayerData(fr.friendID); auto fd = this->GetPlayerData(fr.friendID);
if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0); if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0, fr.isBestFriend);
} }
auto* team = GetTeam(playerID); auto* team = GetTeam(playerID);

View File

@ -14,6 +14,7 @@ struct PlayerData {
LWOZONEID zoneID; LWOZONEID zoneID;
std::vector<FriendData> friends; std::vector<FriendData> friends;
time_t muteExpire; time_t muteExpire;
uint8_t countOfBestFriends = 0;
}; };
struct TeamData { struct TeamData {

View File

@ -0,0 +1,15 @@
#pragma once
#ifndef __ADDFRIENDRESPONSECODE__H__
#define __ADDFRIENDRESPONSECODE__H__
#include <cstdint>
enum class AddFriendResponseCode : uint8_t {
ACCEPTED = 0,
REJECTED,
BUSY,
CANCELLED
};
#endif //!__ADDFRIENDRESPONSECODE__H__

View File

@ -0,0 +1,24 @@
#pragma once
#ifndef __ADDFRIENDRESPONSETYPE__H__
#define __ADDFRIENDRESPONSETYPE__H__
#include <cstdint>
enum class AddFriendResponseType : uint8_t {
ACCEPTED = 0,
ALREADYFRIEND,
INVALIDCHARACTER,
GENERALERROR,
YOURFRIENDSLISTFULL,
THEIRFRIENDLISTFULL,
DECLINED,
BUSY,
NOTONLINE,
WAITINGAPPROVAL,
MYTHRAN,
CANCELLED,
FRIENDISFREETRIAL
};
#endif //!__ADDFRIENDRESPONSETYPE__H__

View File

@ -401,7 +401,7 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr
Game::server->Send(&stream, sysAddr, false); Game::server->Send(&stream, sysAddr, false);
} }
PacketUtils::SavePacket("[24]_"+std::to_string(entity->GetObjectID()) + "_" + std::to_string(m_SerializationCounter) + ".bin", (char*)stream.GetData(), stream.GetNumberOfBytesUsed()); // PacketUtils::SavePacket("[24]_"+std::to_string(entity->GetObjectID()) + "_" + std::to_string(m_SerializationCounter) + ".bin", (char*)stream.GetData(), stream.GetNumberOfBytesUsed());
if (entity->IsPlayer()) if (entity->IsPlayer())
{ {

View File

@ -369,10 +369,8 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet)
} }
LWOOBJID objectID = PacketUtils::ReadPacketS64(8, packet); LWOOBJID objectID = PacketUtils::ReadPacketS64(8, packet);
objectID = GeneralUtils::ClearBit(objectID, OBJECT_BIT_CHARACTER); uint32_t charID = static_cast<uint32_t>(objectID);
objectID = GeneralUtils::ClearBit(objectID, OBJECT_BIT_PERSISTENT);
uint32_t charID = static_cast<uint32_t>(objectID);
Game::logger->Log("UserManager", "Received char delete req for ID: %llu (%u)\n", objectID, charID); Game::logger->Log("UserManager", "Received char delete req for ID: %llu (%u)\n", objectID, charID);
//Check if this user has this character: //Check if this user has this character:
@ -402,10 +400,14 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet)
} }
{ {
sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM friends WHERE player_id=? OR friend_id=?;"); sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM friends WHERE player_id=? OR friend_id=?;");
stmt->setUInt64(1, charID); stmt->setUInt(1, charID);
stmt->setUInt64(2, charID); stmt->setUInt(2, charID);
stmt->execute(); stmt->execute();
delete stmt; delete stmt;
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION);
bitStream.Write(objectID);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
} }
{ {
sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM leaderboard WHERE character_id=?;"); sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM leaderboard WHERE character_id=?;");

View File

@ -102,7 +102,7 @@ PropertySelectQueryProperty PropertyEntranceComponent::SetPropertyValues(Propert
return property; return property;
} }
std::string PropertyEntranceComponent::BuildQuery(Entity* entity, int32_t sortMethod, std::string customQuery, bool wantLimits) { std::string PropertyEntranceComponent::BuildQuery(Entity* entity, int32_t sortMethod, Character* character, std::string customQuery, bool wantLimits) {
std::string base; std::string base;
if (customQuery == "") { if (customQuery == "") {
base = baseQueryForProperties; base = baseQueryForProperties;
@ -115,15 +115,13 @@ std::string PropertyEntranceComponent::BuildQuery(Entity* entity, int32_t sortMe
auto friendsListQuery = Database::CreatePreppedStmt("SELECT * FROM (SELECT CASE WHEN player_id = ? THEN friend_id WHEN friend_id = ? THEN player_id END AS requested_player FROM friends ) AS fr WHERE requested_player IS NOT NULL ORDER BY requested_player DESC;"); auto friendsListQuery = Database::CreatePreppedStmt("SELECT * FROM (SELECT CASE WHEN player_id = ? THEN friend_id WHEN friend_id = ? THEN player_id END AS requested_player FROM friends ) AS fr WHERE requested_player IS NOT NULL ORDER BY requested_player DESC;");
friendsListQuery->setInt64(1, entity->GetObjectID()); friendsListQuery->setUInt(1, character->GetID());
friendsListQuery->setInt64(2, entity->GetObjectID()); friendsListQuery->setUInt(2, character->GetID());
auto friendsListQueryResult = friendsListQuery->executeQuery(); auto friendsListQueryResult = friendsListQuery->executeQuery();
while (friendsListQueryResult->next()) { while (friendsListQueryResult->next()) {
auto playerIDToConvert = friendsListQueryResult->getInt64(1); auto playerIDToConvert = friendsListQueryResult->getInt(1);
playerIDToConvert = GeneralUtils::ClearBit(playerIDToConvert, OBJECT_BIT_CHARACTER);
playerIDToConvert = GeneralUtils::ClearBit(playerIDToConvert, OBJECT_BIT_PERSISTENT);
friendsList = friendsList + std::to_string(playerIDToConvert) + ","; friendsList = friendsList + std::to_string(playerIDToConvert) + ",";
} }
// Replace trailing comma with the closing parenthesis. // Replace trailing comma with the closing parenthesis.
@ -193,7 +191,7 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl
entries.push_back(playerEntry); entries.push_back(playerEntry);
const auto query = BuildQuery(entity, sortMethod); const auto query = BuildQuery(entity, sortMethod, character);
auto propertyLookup = Database::CreatePreppedStmt(query); auto propertyLookup = Database::CreatePreppedStmt(query);
@ -262,17 +260,17 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl
// Query to get friend and best friend fields // Query to get friend and best friend fields
auto friendCheck = Database::CreatePreppedStmt("SELECT best_friend FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?)"); auto friendCheck = Database::CreatePreppedStmt("SELECT best_friend FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?)");
friendCheck->setInt64(1, entity->GetObjectID()); friendCheck->setUInt(1, character->GetID());
friendCheck->setInt64(2, ownerObjId); friendCheck->setUInt(2, ownerObjId);
friendCheck->setInt64(3, ownerObjId); friendCheck->setUInt(3, ownerObjId);
friendCheck->setInt64(4, entity->GetObjectID()); friendCheck->setUInt(4, character->GetID());
auto friendResult = friendCheck->executeQuery(); auto friendResult = friendCheck->executeQuery();
// If we got a result than the two players are friends. // If we got a result than the two players are friends.
if (friendResult->next()) { if (friendResult->next()) {
isFriend = true; isFriend = true;
if (friendResult->getInt(1) == 2) { if (friendResult->getInt(1) == 3) {
isBestFriend = true; isBestFriend = true;
} }
} }
@ -326,7 +324,7 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl
// Query here is to figure out whether or not to display the button to go to the next page or not. // Query here is to figure out whether or not to display the button to go to the next page or not.
int32_t numberOfProperties = 0; int32_t numberOfProperties = 0;
auto buttonQuery = BuildQuery(entity, sortMethod, "SELECT COUNT(*) FROM properties as p JOIN charinfo as ci ON ci.prop_clone_id = p.clone_id where p.zone_id = ? AND (p.description LIKE ? OR p.name LIKE ? OR ci.name LIKE ?) AND p.privacy_option >= ? ", false); auto buttonQuery = BuildQuery(entity, sortMethod, character, "SELECT COUNT(*) FROM properties as p JOIN charinfo as ci ON ci.prop_clone_id = p.clone_id where p.zone_id = ? AND (p.description LIKE ? OR p.name LIKE ? OR ci.name LIKE ?) AND p.privacy_option >= ? ", false);
auto propertiesLeft = Database::CreatePreppedStmt(buttonQuery); auto propertiesLeft = Database::CreatePreppedStmt(buttonQuery);
propertiesLeft->setUInt(1, this->m_MapID); propertiesLeft->setUInt(1, this->m_MapID);

View File

@ -60,7 +60,7 @@ class PropertyEntranceComponent : public Component {
PropertySelectQueryProperty SetPropertyValues(PropertySelectQueryProperty property, LWOCLONEID cloneId = LWOCLONEID_INVALID, std::string ownerName = "", std::string propertyName = "", std::string propertyDescription = "", float reputation = 0, bool isBFF = false, bool isFriend = false, bool isModeratorApproved = false, bool isAlt = false, bool isOwned = false, uint32_t privacyOption = 0, uint32_t timeLastUpdated = 0, float performanceCost = 0.0f); PropertySelectQueryProperty SetPropertyValues(PropertySelectQueryProperty property, LWOCLONEID cloneId = LWOCLONEID_INVALID, std::string ownerName = "", std::string propertyName = "", std::string propertyDescription = "", float reputation = 0, bool isBFF = false, bool isFriend = false, bool isModeratorApproved = false, bool isAlt = false, bool isOwned = false, uint32_t privacyOption = 0, uint32_t timeLastUpdated = 0, float performanceCost = 0.0f);
std::string BuildQuery(Entity* entity, int32_t sortMethod, std::string customQuery = "", bool wantLimits = true); std::string BuildQuery(Entity* entity, int32_t sortMethod, Character* character, std::string customQuery = "", bool wantLimits = true);
private: private:
/** /**

View File

@ -669,10 +669,12 @@ void HandlePacket(Packet* packet) {
Game::logger->Log("WorldServer", "Deleting player %llu\n", entity->GetObjectID()); Game::logger->Log("WorldServer", "Deleting player %llu\n", entity->GetObjectID());
EntityManager::Instance()->DestroyEntity(entity); EntityManager::Instance()->DestroyEntity(entity);
}
{
CBITSTREAM; CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION); PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION);
bitStream.Write(c->GetObjectID()); bitStream.Write(user->GetLoggedInChar());
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
} }
@ -930,6 +932,19 @@ void HandlePacket(Packet* packet) {
playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_CHARACTER); playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_CHARACTER);
playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_PERSISTENT); playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_PERSISTENT);
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 (static_cast<uint32_t>(lastCharacter) != playerID) {
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION);
bitStream.Write(lastCharacter);
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
}
}
UserManager::Instance()->LoginCharacter(packet->systemAddress, static_cast<uint32_t>(playerID)); UserManager::Instance()->LoginCharacter(packet->systemAddress, static_cast<uint32_t>(playerID));
break; break;
} }

View File

@ -60,3 +60,7 @@ classic_survival_scoring=0
# If this value is 1, pets will consume imagination as they did in live. if 0 they will not consume imagination at all. # If this value is 1, pets will consume imagination as they did in live. if 0 they will not consume imagination at all.
pets_take_imagination=1 pets_take_imagination=1
# If you would like to increase the maximum number of best friends a player can have on the server
# Change the value below to what you would like this to be (5 is live accurate)
max_number_of_best_friends=5