mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
Staff arm a packet capture on the dashboard; master passes MESSAGE_CAPTURE_CONTROL ARM to every world, auth and chat and arms its own. Each server's PacketCapture tap (dServer receive, and a send hook in RakPeer::Send so replica constructions are seen too) records into one preallocated chunk per server and ships sealed chunks through master on the main loop when capture_flush_bytes or capture_flush_interval_ms is reached; past capture_buffer_max_mb the oldest chunks are dropped and the dashboard records a gap. Nothing is armed: one flag check. - targets: an account (from its login; packets before the login are kept per connection and added once auth or the world knows whose they are), a character (from when it is picked), or everything; up to 8 at once (a bit each in the record mask) - worlds and auth record their clients' packets and the master link messages of a captured player (session keys by name, zone transfers by request, player added/removed, migration); chat finds the player in each packet; master records server traffic for everything - secrets are never recorded: structs that carry them (login request, login response user key, world validation session key, session key messages between servers) are read, blanked and written again before recording; auth keeps only the handshake and login - PacketDecoder: a registry by service and message id names every packet and decodes the registered structs; CaptureBundle is the file format (DLUBNDL1, metadata, records); CaptureTools orders records on one timeline, pulls movement out, makes bundles portable or anonymous and diffs replays - the dashboard keeps packet captures in message_capture_sessions (capture_kind 1) and their packets in a file under capture_dir, one write per batch; arming is audited - MESSAGE_CAPTURE_CONTROL/DATA only gain appended enum values and trailing fields Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
661 lines
23 KiB
C++
661 lines
23 KiB
C++
#include "UserManager.h"
|
|
#include "PacketCapture.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));
|
|
PacketCapture::Bind(sysAddr, user->GetAccountID(), username);
|
|
// In a zone the user comes with the character they play
|
|
const auto* character = Game::server->GetZoneID() != 0 ? user->GetLastUsedChar() : nullptr;
|
|
if (character) PacketCapture::BindCharacter(sysAddr, character->GetID());
|
|
}
|
|
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();
|
|
|
|
// Newest last login first (charinfo.last_login; a new character counts as logged in when it is made)
|
|
const auto characterIds = Database::Get()->GetAccountCharacterIds(u->GetAccountID());
|
|
for (const auto& characterId : characterIds) {
|
|
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, 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;
|
|
if (!characterIds.empty()) {
|
|
const auto lastUsed = std::ranges::find(chars, characterIds.front(), &Character::GetID);
|
|
if (lastUsed != chars.end()) response.selectedCharacterIndex = static_cast<uint8_t>(lastUsed - chars.begin());
|
|
}
|
|
|
|
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);
|
|
PacketCapture::BindCharacter(sysAddr, 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();
|
|
}
|
|
}
|
|
}
|