mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
647 lines
22 KiB
C++
647 lines
22 KiB
C++
#include "UserManager.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 "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);
|
|
}
|
|
|
|
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 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());
|
|
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 (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());
|
|
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);
|
|
DashboardNotify::Changed("characters", objectID);
|
|
if (!pendingName.empty()) DashboardNotify::Changed("pending_names", objectID);
|
|
|
|
//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);
|
|
DashboardNotify::Changed("characters", 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);
|
|
auto newName = LUWStringName.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) {
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
|
|
} 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()) {
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE);
|
|
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());
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
|
UserManager::RequestCharacterList(sysAddr);
|
|
} else if (IsNamePreapproved(newName)) {
|
|
Database::Get()->SetCharacterName(objectID, newName);
|
|
DashboardNotify::Changed("characters", objectID);
|
|
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
|
UserManager::RequestCharacterList(sysAddr);
|
|
} else {
|
|
Database::Get()->SetPendingCharacterName(objectID, newName);
|
|
DashboardNotify::Changed("characters", objectID);
|
|
DashboardNotify::Changed("pending_names", objectID);
|
|
LOG("Character %s has been renamed to %s and is pending approval by a moderator.", character->GetName().c_str(), newName.c_str());
|
|
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
|
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();
|
|
}
|
|
}
|
|
}
|