mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-10-10 17:38:08 +00:00

* feat: re-write persistent object ID tracker Features: - Remove random objectIDs entirely - Replace random objectIDs with persistentIDs - Remove the need to contact the MASTER server for a persistent ID - Add persistent ID logic to WorldServers that use transactions to guarantee unique IDs no matter when they are generated - Default character xml version to be the most recent one Fixes: - Return optional from GetModel (and check for nullopt where it may exist) - Regenerate inventory item ids on first login to be unique item IDs (fixes all those random IDs Pet IDs and subkeys are left alone and are assumed to be reserved (checks are there to prevent this) There is also duplicate check logic in place for properties and UGC/Models * Update comment and log * fix: sqlite transaction bug * fix colliding temp item ids temp items should not be saved. would cause issues between worlds as experienced before this commit
588 lines
19 KiB
C++
588 lines
19 KiB
C++
#include "UserManager.h"
|
|
#include <fstream>
|
|
#include <future>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
|
|
#include "Database.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "User.h"
|
|
#include "WorldPackets.h"
|
|
#include "Character.h"
|
|
#include "BitStream.h"
|
|
#include "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 "ServiceType.h"
|
|
#include "MessageType/Chat.h"
|
|
#include "BitStreamUtils.h"
|
|
#include "CheatDetection.h"
|
|
#include "CharacterComponent.h"
|
|
#include "eCharacterVersion.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;
|
|
|
|
auto fnStream = Game::assetManager->GetFile("names/minifigname_first.txt");
|
|
if (!fnStream) {
|
|
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.");
|
|
}
|
|
|
|
while (std::getline(fnStream, line, '\n')) {
|
|
std::string name = line;
|
|
StripCR(name);
|
|
m_FirstNames.push_back(name);
|
|
}
|
|
|
|
auto mnStream = Game::assetManager->GetFile("names/minifigname_middle.txt");
|
|
if (!mnStream) {
|
|
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.");
|
|
}
|
|
|
|
while (std::getline(mnStream, line, '\n')) {
|
|
std::string name = line;
|
|
StripCR(name);
|
|
m_MiddleNames.push_back(name);
|
|
}
|
|
|
|
auto lnStream = Game::assetManager->GetFile("names/minifigname_last.txt");
|
|
if (!lnStream) {
|
|
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.");
|
|
}
|
|
|
|
while (std::getline(lnStream, line, '\n')) {
|
|
std::string name = line;
|
|
StripCR(name);
|
|
m_LastNames.push_back(name);
|
|
}
|
|
|
|
// Load our pre-approved names:
|
|
auto chatListStream = Game::assetManager->GetFile("chatplus_en_us.txt");
|
|
if (!chatListStream) {
|
|
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 allowlist file.");
|
|
}
|
|
|
|
while (std::getline(chatListStream, line, '\n')) {
|
|
StripCR(line);
|
|
m_PreapprovedNames.push_back(line);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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;
|
|
std::vector<Character*>& 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<SkillComponent>();
|
|
|
|
if (skillComponent != nullptr) {
|
|
skillComponent->Reset();
|
|
}
|
|
|
|
Game::entityManager->DestroyEntity(chars[i]->GetEntity());
|
|
|
|
chars[i]->SaveXMLToDatabase();
|
|
|
|
chars[i]->GetEntity()->SetCharacter(nullptr);
|
|
|
|
delete chars[i];
|
|
}
|
|
|
|
chars.clear();
|
|
|
|
for (const auto& characterId : Database::Get()->GetAccountCharacterIds(u->GetAccountID())) {
|
|
Character* character = new Character(characterId, u);
|
|
character->UpdateFromDatabase();
|
|
character->SetIsNewLogin();
|
|
chars.push_back(character);
|
|
}
|
|
|
|
RakNet::BitStream bitStream;
|
|
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE);
|
|
|
|
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 (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;
|
|
}
|
|
|
|
void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) {
|
|
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 name = LUWStringName.GetAsString();
|
|
std::string predefinedName = GetPredefinedName(firstNameIndex, middleNameIndex, lastNameIndex);
|
|
|
|
LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle);
|
|
LOT pantsLOT = FindCharPantsID(pantsColor);
|
|
|
|
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);
|
|
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);
|
|
return;
|
|
}
|
|
|
|
if (name.empty()) {
|
|
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 a persistent ObjectID:
|
|
LWOOBJID objectID = ObjectIDManager::GetPersistentID();
|
|
const uint32_t maxRetries = 100;
|
|
uint32_t tries = 0;
|
|
while (Database::Get()->GetCharacterInfo(objectID) && tries < maxRetries) {
|
|
tries++;
|
|
LOG("Found a duplicate character %llu, getting a new objectID", objectID);
|
|
objectID = ObjectIDManager::GetPersistentID();
|
|
}
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
std::stringstream xml;
|
|
xml << "<obj v=\"1\">";
|
|
|
|
xml << "<mf hc=\"" << hairColor << "\" hs=\"" << hairStyle << "\" hd=\"0\" t=\"" << shirtColor << "\" l=\"" << pantsColor;
|
|
xml << "\" hdc=\"0\" cd=\"" << shirtStyle << "\" lh=\"" << lh << "\" rh=\"" << rh << "\" es=\"" << eyebrows << "\" ";
|
|
xml << "ess=\"" << eyes << "\" ms=\"" << mouth << "\"/>";
|
|
|
|
xml << "<char acct=\"" << u->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 << "<vl><l id=\"1000\" cid=\"0\"/></vl>";
|
|
|
|
xml << "</char>";
|
|
|
|
xml << "<dest hm=\"4\" hc=\"4\" im=\"0\" ic=\"0\" am=\"0\" ac=\"0\" d=\"0\"/>";
|
|
|
|
xml << "<inv><bag><b t=\"0\" m=\"20\"/><b t=\"1\" m=\"40\"/><b t=\"2\" m=\"240\"/><b t=\"3\" m=\"240\"/><b t=\"14\" m=\"40\"/></bag><items><in t=\"0\">";
|
|
|
|
LWOOBJID lwoidforshirt = ObjectIDManager::GetPersistentID();
|
|
LWOOBJID lwoidforpants = ObjectIDManager::GetPersistentID();
|
|
|
|
xml << "<i l=\"" << shirtLOT << "\" id=\"" << lwoidforshirt << "\" s=\"0\" c=\"1\" eq=\"1\" b=\"1\"/>";
|
|
xml << "<i l=\"" << pantsLOT << "\" id=\"" << lwoidforpants << "\" s=\"1\" c=\"1\" eq=\"1\" b=\"1\"/>";
|
|
|
|
xml << "</in></items></inv><lvl l=\"1\" cv=\"" << GeneralUtils::ToUnderlying(eCharacterVersion::UP_TO_DATE) << "\" sb=\"500\"/><flag></flag></obj>";
|
|
|
|
//Check to see if our name was pre-approved:
|
|
bool nameOk = IsNamePreapproved(name);
|
|
if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true;
|
|
|
|
// If predefined name is invalid, change it to be their object id
|
|
// that way more than one player can create characters if the predefined name files are not provided
|
|
auto assignedPredefinedName = predefinedName;
|
|
if (assignedPredefinedName == "INVALID") {
|
|
std::stringstream nameObjID;
|
|
nameObjID << "minifig" << objectID;
|
|
assignedPredefinedName = nameObjID.str();
|
|
}
|
|
|
|
std::string_view nameToAssign = !name.empty() && nameOk ? name : assignedPredefinedName;
|
|
std::string pendingName = !name.empty() && !nameOk ? name : "";
|
|
|
|
ICharInfo::Info info;
|
|
info.name = nameToAssign;
|
|
info.pendingName = pendingName;
|
|
info.id = objectID;
|
|
info.accountId = u->GetAccountID();
|
|
|
|
Database::Get()->InsertNewCharacter(info);
|
|
|
|
//Now finally insert our character xml:
|
|
Database::Get()->InsertCharacterXml(objectID, xml.str());
|
|
|
|
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;
|
|
}
|
|
|
|
CINSTREAM_SKIP_HEADER;
|
|
LWOOBJID objectID;
|
|
inStream.Read(objectID);
|
|
|
|
LOG("Received char delete req for ID: %llu", objectID);
|
|
|
|
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 %llu", objectID);
|
|
Database::Get()->DeleteCharacter(objectID);
|
|
|
|
CBITSTREAM;
|
|
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT);
|
|
bitStream.Write(objectID);
|
|
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
|
|
|
|
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;
|
|
}
|
|
|
|
CINSTREAM_SKIP_HEADER;
|
|
LWOOBJID objectID;
|
|
inStream.Read(objectID);
|
|
|
|
LOG("Received char rename request for ID: %llu", objectID);
|
|
|
|
LUWString LUWStringName;
|
|
inStream.Read(LUWStringName);
|
|
const auto newName = LUWStringName.GetAsString();
|
|
|
|
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());
|
|
|
|
auto unusedItr = std::find_if(u->GetCharacters().begin(), u->GetCharacters().end(), [&](Character* c) {
|
|
if (c->GetID() == objectID) {
|
|
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 (!Database::Get()->GetCharacterInfo(newName)) {
|
|
if (IsNamePreapproved(newName)) {
|
|
Database::Get()->SetCharacterName(objectID, newName);
|
|
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
|
UserManager::RequestCharacterList(sysAddr);
|
|
} else {
|
|
Database::Get()->SetPendingCharacterName(objectID, newName);
|
|
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, LWOOBJID 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<Character*>& 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) {
|
|
Database::Get()->UpdateLastLoggedInCharacter(playerID);
|
|
|
|
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) {
|
|
auto* entity = Game::entityManager->GetEntity(character->GetObjectID());
|
|
if (entity) {
|
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
|
if (characterComponent) {
|
|
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
|
}
|
|
}
|
|
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 {
|
|
auto stmt = CDClientDatabase::CreatePreppedStmt(
|
|
"select obj.id as objectId 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) == ? AND icc.color1 == ? AND icc.decal == ?"
|
|
);
|
|
stmt.bind(1, "character create shirt");
|
|
stmt.bind(2, static_cast<int>(shirtColor));
|
|
stmt.bind(3, static_cast<int>(shirtStyle));
|
|
auto tableData = stmt.execQuery();
|
|
auto shirtLOT = tableData.getIntField("objectId", 4069);
|
|
tableData.finalize();
|
|
return shirtLOT;
|
|
} catch (const std::exception& ex) {
|
|
LOG("Could not look up shirt %i %i: %s", shirtColor, shirtStyle, ex.what());
|
|
// in case of no shirt found in CDServer, return problematic red vest.
|
|
return 4069;
|
|
}
|
|
}
|
|
|
|
uint32_t FindCharPantsID(uint32_t pantsColor) {
|
|
try {
|
|
auto stmt = CDClientDatabase::CreatePreppedStmt(
|
|
"select obj.id as objectId 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) == ? AND icc.color1 == ?"
|
|
);
|
|
stmt.bind(1, "cc pants");
|
|
stmt.bind(2, static_cast<int>(pantsColor));
|
|
auto tableData = stmt.execQuery();
|
|
auto pantsLOT = tableData.getIntField("objectId", 2508);
|
|
tableData.finalize();
|
|
return pantsLOT;
|
|
} catch (const std::exception& ex) {
|
|
LOG("Could not look up pants %i: %s", pantsColor, ex.what());
|
|
// 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();
|
|
}
|
|
}
|
|
}
|