#include "UserManager.h" #include #include #include #include #include "Database.h" #include "Game.h" #include "Logger.h" #include "User.h" #include #include "Character.h" #include #include "PacketUtils.h" #include "../dWorldServer/ObjectIDManager.h" #include "Logger.h" #include "GeneralUtils.h" #include "ZoneInstanceManager.h" #include "dServer.h" #include "Entity.h" #include "EntityManager.h" #include "SkillComponent.h" #include "AssetManager.h" #include "CDClientDatabase.h" #include "eObjectBits.h" #include "eGameMasterLevel.h" #include "eCharacterCreationResponse.h" #include "eRenameResponse.h" #include "eConnectionType.h" #include "eChatInternalMessageType.h" #include "BitStreamUtils.h" #include "CheatDetection.h" UserManager* UserManager::m_Address = nullptr; //Local functions as they aren't needed by anything else, leave the implementations at the bottom! uint32_t FindCharShirtID(uint32_t shirtColor, uint32_t shirtStyle); uint32_t FindCharPantsID(uint32_t pantsColor); inline void StripCR(std::string& str) { str.erase(std::remove(str.begin(), str.end(), '\r'), str.end()); } void UserManager::Initialize() { std::string line; AssetMemoryBuffer fnBuff = Game::assetManager->GetFileAsBuffer("names/minifigname_first.txt"); if (!fnBuff.m_Success) { LOG("Failed to load %s", (Game::assetManager->GetResPath() / "names/minifigname_first.txt").string().c_str()); throw std::runtime_error("Aborting initialization due to missing minifigure name file."); } std::istream fnStream = std::istream(&fnBuff); while (std::getline(fnStream, line, '\n')) { std::string name = line; StripCR(name); m_FirstNames.push_back(name); } fnBuff.close(); AssetMemoryBuffer mnBuff = Game::assetManager->GetFileAsBuffer("names/minifigname_middle.txt"); if (!mnBuff.m_Success) { LOG("Failed to load %s", (Game::assetManager->GetResPath() / "names/minifigname_middle.txt").string().c_str()); throw std::runtime_error("Aborting initialization due to missing minifigure name file."); } std::istream mnStream = std::istream(&mnBuff); while (std::getline(mnStream, line, '\n')) { std::string name = line; StripCR(name); m_MiddleNames.push_back(name); } mnBuff.close(); AssetMemoryBuffer lnBuff = Game::assetManager->GetFileAsBuffer("names/minifigname_last.txt"); if (!lnBuff.m_Success) { LOG("Failed to load %s", (Game::assetManager->GetResPath() / "names/minifigname_last.txt").string().c_str()); throw std::runtime_error("Aborting initialization due to missing minifigure name file."); } std::istream lnStream = std::istream(&lnBuff); while (std::getline(lnStream, line, '\n')) { std::string name = line; StripCR(name); m_LastNames.push_back(name); } lnBuff.close(); //Load our pre-approved names: AssetMemoryBuffer chatListBuff = Game::assetManager->GetFileAsBuffer("chatplus_en_us.txt"); if (!chatListBuff.m_Success) { LOG("Failed to load %s", (Game::assetManager->GetResPath() / "chatplus_en_us.txt").string().c_str()); throw std::runtime_error("Aborting initialization due to missing chat whitelist file."); } std::istream chatListStream = std::istream(&chatListBuff); while (std::getline(chatListStream, line, '\n')) { StripCR(line); m_PreapprovedNames.push_back(line); } chatListBuff.close(); } UserManager::~UserManager() { } User* UserManager::CreateUser(const SystemAddress& sysAddr, const std::string& username, const std::string& sessionKey) { User* user = new User(sysAddr, username, sessionKey); if (user && Game::server->IsConnected(sysAddr)) m_Users.insert(std::make_pair(sysAddr, user)); else { if (user) { delete user; user = nullptr; } } return user; } User* UserManager::GetUser(const SystemAddress& sysAddr) { auto it = m_Users.find(sysAddr); if (it != m_Users.end() && it->second) return it->second; return nullptr; } User* UserManager::GetUser(const std::string& username) { for (auto p : m_Users) { if (p.second) { if (GeneralUtils::CaseInsensitiveStringCompare(p.second->GetUsername(), username)) return p.second; } } return nullptr; } bool UserManager::DeleteUser(const SystemAddress& sysAddr) { const auto& it = m_Users.find(sysAddr); if (it != m_Users.end()) { if (std::count(m_UsersToDelete.begin(), m_UsersToDelete.end(), it->second)) return false; m_UsersToDelete.push_back(it->second); m_Users.erase(it); return true; } return false; } void UserManager::DeletePendingRemovals() { for (auto* user : m_UsersToDelete) { LOG("Deleted user %i", user->GetAccountID()); delete user; } m_UsersToDelete.clear(); } bool UserManager::IsNameAvailable(const std::string& requestedName) { bool toReturn = false; //To allow for a clean exit sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT id FROM charinfo WHERE name=? OR pending_name=? LIMIT 1;"); stmt->setString(1, requestedName.c_str()); stmt->setString(2, requestedName.c_str()); sql::ResultSet* res = stmt->executeQuery(); if (res->rowsCount() == 0) toReturn = true; delete stmt; delete res; return toReturn; } std::string UserManager::GetPredefinedName(uint32_t firstNameIndex, uint32_t middleNameIndex, uint32_t lastNameIndex) { if (firstNameIndex > m_FirstNames.size() || middleNameIndex > m_MiddleNames.size() || lastNameIndex > m_LastNames.size()) return std::string("INVALID"); return std::string(m_FirstNames[firstNameIndex] + m_MiddleNames[middleNameIndex] + m_LastNames[lastNameIndex]); } bool UserManager::IsNamePreapproved(const std::string& requestedName) { for (std::string& s : m_PreapprovedNames) { if (s == requestedName) return true; } for (std::string& s : m_FirstNames) { if (s == requestedName) return true; } for (std::string& s : m_MiddleNames) { if (s == requestedName) return true; } for (std::string& s : m_LastNames) { if (s == requestedName) return true; } return false; } void UserManager::RequestCharacterList(const SystemAddress& sysAddr) { User* u = GetUser(sysAddr); if (!u) return; sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT id FROM charinfo WHERE account_id=? ORDER BY last_login DESC LIMIT 4;"); stmt->setUInt(1, u->GetAccountID()); sql::ResultSet* res = stmt->executeQuery(); std::vector& chars = u->GetCharacters(); for (size_t i = 0; i < chars.size(); ++i) { if (chars[i]->GetEntity() == nullptr) // We don't have entity data to save { delete chars[i]; continue; } auto* skillComponent = chars[i]->GetEntity()->GetComponent(); if (skillComponent != nullptr) { skillComponent->Reset(); } Game::entityManager->DestroyEntity(chars[i]->GetEntity()); chars[i]->SaveXMLToDatabase(); chars[i]->GetEntity()->SetCharacter(nullptr); delete chars[i]; } chars.clear(); while (res->next()) { LWOOBJID objID = res->getUInt64(1); Character* character = new Character(uint32_t(objID), u); character->SetIsNewLogin(); chars.push_back(character); } delete res; delete stmt; WorldPackets::SendCharacterList(sysAddr, u); } void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) { User* u = GetUser(sysAddr); if (!u) return; std::string name = PacketUtils::ReadString(8, packet, true); uint32_t firstNameIndex = PacketUtils::ReadU32(74, packet); uint32_t middleNameIndex = PacketUtils::ReadU32(78, packet); uint32_t lastNameIndex = PacketUtils::ReadU32(82, packet); std::string predefinedName = GetPredefinedName(firstNameIndex, middleNameIndex, lastNameIndex); uint32_t shirtColor = PacketUtils::ReadU32(95, packet); uint32_t shirtStyle = PacketUtils::ReadU32(99, packet); uint32_t pantsColor = PacketUtils::ReadU32(103, packet); uint32_t hairStyle = PacketUtils::ReadU32(107, packet); uint32_t hairColor = PacketUtils::ReadU32(111, packet); uint32_t lh = PacketUtils::ReadU32(115, packet); uint32_t rh = PacketUtils::ReadU32(119, packet); uint32_t eyebrows = PacketUtils::ReadU32(123, packet); uint32_t eyes = PacketUtils::ReadU32(127, packet); uint32_t mouth = PacketUtils::ReadU32(131, packet); LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle); LOT pantsLOT = FindCharPantsID(pantsColor); if (name != "" && !UserManager::IsNameAvailable(name)) { LOG("AccountID: %i chose unavailable name: %s", u->GetAccountID(), name.c_str()); WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::CUSTOM_NAME_IN_USE); return; } if (!IsNameAvailable(predefinedName)) { LOG("AccountID: %i chose unavailable predefined name: %s", u->GetAccountID(), predefinedName.c_str()); WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::PREDEFINED_NAME_IN_USE); return; } if (name == "") { LOG("AccountID: %i is creating a character with predefined name: %s", u->GetAccountID(), predefinedName.c_str()); } else { LOG("AccountID: %i is creating a character with name: %s (temporary: %s)", u->GetAccountID(), name.c_str(), predefinedName.c_str()); } //Now that the name is ok, we can get an objectID from Master: ObjectIDManager::Instance()->RequestPersistentID([=](uint32_t objectID) { sql::PreparedStatement* overlapStmt = Database::CreatePreppedStmt("SELECT id FROM charinfo WHERE id = ?"); overlapStmt->setUInt(1, objectID); auto* overlapResult = overlapStmt->executeQuery(); if (overlapResult->next()) { LOG("Character object id unavailable, check objectidtracker!"); WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE); return; } std::stringstream xml; xml << ""; xml << "GetAccountID() << "\" cc=\"0\" gm=\"0\" ft=\"0\" llog=\"" << time(NULL) << "\" "; xml << "ls=\"0\" lzx=\"-626.5847\" lzy=\"613.3515\" lzz=\"-28.6374\" lzrx=\"0.0\" lzry=\"0.7015\" lzrz=\"0.0\" lzrw=\"0.7126\" "; xml << "stt=\"0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;\">"; xml << ""; xml << ""; std::string xmlSave1 = xml.str(); ObjectIDManager::Instance()->RequestPersistentID([=](uint32_t idforshirt) { std::stringstream xml2; LWOOBJID lwoidforshirt = idforshirt; GeneralUtils::SetBit(lwoidforshirt, eObjectBits::CHARACTER); GeneralUtils::SetBit(lwoidforshirt, eObjectBits::PERSISTENT); xml2 << xmlSave1 << ""; std::string xmlSave2 = xml2.str(); ObjectIDManager::Instance()->RequestPersistentID([=](uint32_t idforpants) { LWOOBJID lwoidforpants = idforpants; GeneralUtils::SetBit(lwoidforpants, eObjectBits::CHARACTER); GeneralUtils::SetBit(lwoidforpants, eObjectBits::PERSISTENT); std::stringstream xml3; xml3 << xmlSave2 << ""; xml3 << ""; //Check to see if our name was pre-approved: bool nameOk = IsNamePreapproved(name); if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true; if (name != "") { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)"); stmt->setUInt(1, objectID); stmt->setUInt(2, u->GetAccountID()); stmt->setString(3, predefinedName.c_str()); stmt->setString(4, name.c_str()); stmt->setBoolean(5, false); stmt->setUInt64(6, time(NULL)); if (nameOk) { stmt->setString(3, name.c_str()); stmt->setString(4, ""); } stmt->execute(); delete stmt; } else { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)"); stmt->setUInt(1, objectID); stmt->setUInt(2, u->GetAccountID()); stmt->setString(3, predefinedName.c_str()); stmt->setString(4, ""); stmt->setBoolean(5, false); stmt->setUInt64(6, time(NULL)); stmt->execute(); delete stmt; } //Now finally insert our character xml: sql::PreparedStatement* stmt = Database::CreatePreppedStmt("INSERT INTO `charxml`(`id`, `xml_data`) VALUES (?,?)"); stmt->setUInt(1, objectID); stmt->setString(2, xml3.str().c_str()); stmt->execute(); delete stmt; WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::SUCCESS); UserManager::RequestCharacterList(sysAddr); }); }); }); } void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) { User* u = GetUser(sysAddr); if (!u) { LOG("Couldn't get user to delete character"); return; } LWOOBJID objectID = PacketUtils::ReadS64(8, packet); uint32_t charID = static_cast(objectID); LOG("Received char delete req for ID: %llu (%u)", objectID, charID); bool hasCharacter = CheatDetection::VerifyLwoobjidIsSender( objectID, sysAddr, CheckType::User, "User %i tried to delete a character that it does not own!", u->GetAccountID()); if (!hasCharacter) { WorldPackets::SendCharacterDeleteResponse(sysAddr, false); } else { LOG("Deleting character %i", charID); { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM charxml WHERE id=? LIMIT 1;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM command_log WHERE character_id=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM friends WHERE player_id=? OR friend_id=?;"); stmt->setUInt(1, charID); stmt->setUInt(2, charID); stmt->execute(); delete stmt; CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::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=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt( "DELETE FROM properties_contents WHERE property_id IN (SELECT id FROM properties WHERE owner_id=?);" ); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM properties WHERE owner_id=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM ugc WHERE character_id=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM activity_log WHERE character_id=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM mail WHERE receiver_id=?;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("DELETE FROM charinfo WHERE id=? LIMIT 1;"); stmt->setUInt64(1, charID); stmt->execute(); delete stmt; } WorldPackets::SendCharacterDeleteResponse(sysAddr, true); } } void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) { User* u = GetUser(sysAddr); if (!u) { LOG("Couldn't get user to delete character"); return; } LWOOBJID objectID = PacketUtils::ReadS64(8, packet); GeneralUtils::ClearBit(objectID, eObjectBits::CHARACTER); GeneralUtils::ClearBit(objectID, eObjectBits::PERSISTENT); uint32_t charID = static_cast(objectID); LOG("Received char rename request for ID: %llu (%u)", objectID, charID); std::string newName = PacketUtils::ReadString(16, packet, true); Character* character = nullptr; //Check if this user has this character: bool ownsCharacter = CheatDetection::VerifyLwoobjidIsSender( objectID, sysAddr, CheckType::User, "User %i tried to rename a character that it does not own!", u->GetAccountID()); std::find_if(u->GetCharacters().begin(), u->GetCharacters().end(), [&](Character* c) { if (c->GetID() == charID) { character = c; return true; } return false; }); if (!ownsCharacter || !character) { WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR); } else if (ownsCharacter && character) { if (newName == character->GetName()) { WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE); return; } if (IsNameAvailable(newName)) { if (IsNamePreapproved(newName)) { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("UPDATE charinfo SET name=?, pending_name='', needs_rename=0, last_login=? WHERE id=? LIMIT 1"); stmt->setString(1, newName); stmt->setUInt64(2, time(NULL)); stmt->setUInt(3, character->GetID()); stmt->execute(); delete stmt; LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str()); WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS); UserManager::RequestCharacterList(sysAddr); } else { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("UPDATE charinfo SET pending_name=?, needs_rename=0, last_login=? WHERE id=? LIMIT 1"); stmt->setString(1, newName); stmt->setUInt64(2, time(NULL)); stmt->setUInt(3, character->GetID()); stmt->execute(); delete stmt; 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); UserManager::RequestCharacterList(sysAddr); } } else { WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_IN_USE); } } else { LOG("Unknown error occurred when renaming character, either hasCharacter or character variable != true."); WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR); } } void UserManager::LoginCharacter(const SystemAddress& sysAddr, uint32_t playerID) { User* u = GetUser(sysAddr); if (!u) { LOG("Couldn't get user to log in character"); return; } Character* character = nullptr; bool hasCharacter = false; std::vector& characters = u->GetCharacters(); for (size_t i = 0; i < characters.size(); ++i) { if (characters[i]->GetID() == playerID) { hasCharacter = true; character = characters[i]; } } if (hasCharacter && character) { sql::PreparedStatement* stmt = Database::CreatePreppedStmt("UPDATE charinfo SET last_login=? WHERE id=? LIMIT 1"); stmt->setUInt64(1, time(NULL)); stmt->setUInt(2, playerID); stmt->execute(); delete stmt; uint32_t zoneID = character->GetZoneID(); if (zoneID == LWOZONEID_INVALID) zoneID = 1000; //Send char to VE ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (character) { character->SetZoneID(zoneID); character->SetZoneInstance(zoneInstance); character->SetZoneClone(zoneClone); } WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); return; }); } else { LOG("Unknown error occurred when logging in a character, either hasCharacter or character variable != true."); } } uint32_t FindCharShirtID(uint32_t shirtColor, uint32_t shirtStyle) { try { std::string shirtQuery = "select obj.id from Objects as obj JOIN (select * from ComponentsRegistry as cr JOIN ItemComponent as ic on ic.id = cr.component_id where cr.component_type == 11) as icc on icc.id = obj.id where lower(obj._internalNotes) == \"character create shirt\" AND icc.color1 == "; shirtQuery += std::to_string(shirtColor); shirtQuery += " AND icc.decal == "; shirtQuery = shirtQuery + std::to_string(shirtStyle); auto tableData = CDClientDatabase::ExecuteQuery(shirtQuery); auto shirtLOT = tableData.getIntField(0, -1); tableData.finalize(); return shirtLOT; } catch (const std::exception&) { LOG("Failed to execute query! Using backup..."); // in case of no shirt found in CDServer, return problematic red vest. return 4069; } } uint32_t FindCharPantsID(uint32_t pantsColor) { try { std::string pantsQuery = "select obj.id from Objects as obj JOIN (select * from ComponentsRegistry as cr JOIN ItemComponent as ic on ic.id = cr.component_id where cr.component_type == 11) as icc on icc.id = obj.id where lower(obj._internalNotes) == \"cc pants\" AND icc.color1 == "; pantsQuery += std::to_string(pantsColor); auto tableData = CDClientDatabase::ExecuteQuery(pantsQuery); auto pantsLOT = tableData.getIntField(0, -1); tableData.finalize(); return pantsLOT; } catch (const std::exception&) { LOG("Failed to execute query! Using backup..."); // in case of no pants color found in CDServer, return red pants. return 2508; } } void UserManager::SaveAllActiveCharacters() { for (auto user : m_Users) { if (user.second) { auto character = user.second->GetLastUsedChar(); if (character) character->SaveXMLToDatabase(); } } }