mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
The character list selected index 0 and was ordered by last login, so the characters swapped places on the selection screen after each session. Live kept them in the order they were made and selected the one with the latest last login (a new character counts as logged in when it is made): 2014 captures show e.g. a new fourth character appended and selected (index 3), and after the next login the list in the same order with index 0 for the character played last. Characters are now sorted by ID (creation order) and the one with the latest last login is selected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
652 lines
23 KiB
C++
652 lines
23 KiB
C++
#include "UserManager.h"
|
|
#include "ChatServerLink.h"
|
|
#include "ChatPackets.h"
|
|
#include "DashboardNotify.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 "ClientPackets.h"
|
|
#include "ClientPackets.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 "dConfig.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);
|
|
}
|
|
|
|
// Initialize cached config values and register a handler to update them on config reload
|
|
// This avoids repeated lookups into dConfig at runtime.
|
|
if (Game::config) {
|
|
m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
|
m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
|
m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
|
|
|
Game::config->AddConfigHandler([this]() {
|
|
this->m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
|
this->m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
|
this->m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
|
});
|
|
}
|
|
else {
|
|
LOG("Warning: dConfig not initialized before UserManager. Cached config values will not be available.");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
std::vector<User*> UserManager::GetUsersForAccount(uint32_t accountId) {
|
|
std::vector<User*> users;
|
|
for (const auto& [_, user] : m_Users) {
|
|
if (user && user->GetAccountID() == accountId) users.push_back(user);
|
|
}
|
|
return users;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Live kept the characters in the order they were made and selected the one with the latest last login (a new
|
|
// character counts as logged in when it is made), as 2014 captures of the character list show. Character IDs
|
|
// only grow, so sorting by ID gives the order they were made in.
|
|
std::ranges::sort(chars, {}, &Character::GetID);
|
|
|
|
ClientPackets::CharacterListResponse response;
|
|
response.selectedCharacterIndex = 0;
|
|
for (size_t i = 1; i < chars.size(); i++) {
|
|
if (chars[i]->GetLastLogin() > chars[response.selectedCharacterIndex]->GetLastLogin()) response.selectedCharacterIndex = static_cast<uint8_t>(i);
|
|
}
|
|
|
|
for (const auto* const character : u->GetCharacters()) {
|
|
auto& entry = response.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();
|
|
}
|
|
|
|
response.Send(sysAddr);
|
|
}
|
|
|
|
void UserManager::CreateCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterCreateRequest& request) {
|
|
User* u = GetUser(sysAddr);
|
|
if (!u) return;
|
|
|
|
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();
|
|
|
|
const auto name = autoRejectNames ? "" : 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());
|
|
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());
|
|
ClientPackets::CharacterCreateResponse response;
|
|
response.response = eCharacterCreationResponse::PREDEFINED_NAME_IN_USE;
|
|
response.Send(sysAddr);
|
|
return;
|
|
}
|
|
|
|
if (autoRejectNames) {
|
|
LOG("AccountID: %i is muted, forcing use of predefined name", u->GetAccountID());
|
|
}
|
|
|
|
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());
|
|
ClientPackets::CharacterCreateResponse response;
|
|
response.response = eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE;
|
|
response.Send(sysAddr);
|
|
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);
|
|
DashboardNotify::Changed("characters", objectID);
|
|
if (!pendingName.empty()) DashboardNotify::Changed("pending_names", objectID);
|
|
|
|
//Now finally insert our character xml:
|
|
Database::Get()->InsertCharacterXml(objectID, xml.str());
|
|
|
|
ClientPackets::CharacterCreateResponse response;
|
|
response.response = eCharacterCreationResponse::SUCCESS;
|
|
response.Send(sysAddr);
|
|
UserManager::RequestCharacterList(sysAddr);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const LWOOBJID objectID = request.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) {
|
|
ClientPackets::DeleteCharacterResponse response;
|
|
response.success = false;
|
|
response.Send(sysAddr);
|
|
} else {
|
|
LOG("Deleting character %llu", objectID);
|
|
Database::Get()->DeleteCharacter(objectID);
|
|
DashboardNotify::Changed("characters", objectID);
|
|
|
|
ChatPackets::UnexpectedDisconnect notify;
|
|
notify.playerID = objectID;
|
|
ChatServerLink::Send(notify);
|
|
|
|
ClientPackets::DeleteCharacterResponse response;
|
|
response.success = true;
|
|
response.Send(sysAddr);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const LWOOBJID objectID = request.objectID;
|
|
|
|
LOG("Received char rename request for ID: %llu", objectID);
|
|
|
|
auto newName = request.name.GetAsString();
|
|
|
|
Character* character = nullptr;
|
|
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
|
|
|
|
//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) {
|
|
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)
|
|
if (!m_FirstNames.empty() && !m_MiddleNames.empty() && !m_LastNames.empty()) {
|
|
std::string firstName = GeneralUtils::GetRandomElement(m_FirstNames);
|
|
std::string middleName = GeneralUtils::GetRandomElement(m_MiddleNames);
|
|
std::string lastName = GeneralUtils::GetRandomElement(m_LastNames);
|
|
newName = firstName + middleName + lastName;
|
|
} else {
|
|
newName = "character" + std::to_string(objectID);
|
|
}
|
|
}
|
|
|
|
if (newName == character->GetName()) {
|
|
ClientPackets::CharacterRenameResponse response;
|
|
response.response = eRenameResponse::NAME_UNAVAILABLE;
|
|
response.Send(sysAddr);
|
|
return;
|
|
}
|
|
|
|
if (!Database::Get()->IsNameInUse(newName)) {
|
|
if (autoRejectNames) {
|
|
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());
|
|
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());
|
|
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());
|
|
ClientPackets::CharacterRenameResponse response;
|
|
response.response = eRenameResponse::SUCCESS;
|
|
response.Send(sysAddr);
|
|
UserManager::RequestCharacterList(sysAddr);
|
|
}
|
|
} else {
|
|
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.");
|
|
ClientPackets::CharacterRenameResponse response;
|
|
response.response = eRenameResponse::UNKNOWN_ERROR;
|
|
response.Send(sysAddr);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
ClientPackets::TransferToWorld transfer;
|
|
transfer.serverIP = LUString(serverIP);
|
|
transfer.serverPort = serverPort;
|
|
transfer.mythranShift = mythranShift;
|
|
transfer.Send(sysAddr);
|
|
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();
|
|
}
|
|
}
|
|
}
|