mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h (docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of the world server reads or writes a packet by hand any more. - World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute, Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds), AchievementNotify, ShowAllRequest, FindPlayerRequest. - Client -> world -> chat (friends, ignore list, teams, general and private chat): GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList, AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite, TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus. The 77 bytes the handlers skipped are named fields now (the sender block the client fills in); the 4 unused bytes after the player ID are kept as `unknown`. - What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage (replaces SendChatMessage; SendSystemMessage stays as a helper built on it), PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest, AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse, ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse, and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse, TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg structs, since chat doesn't link dGame's NetGameMsg. - WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h, since it carries packets of other services): the target and the inner packet. ChatPacketHandler::SendRouted replaces the per-function route headers and SendRoutedMsg. Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler function); packets that fail to Deserialize are logged and dropped. The chat server's switch and the world's HandlePacketChat switch are now maps. The handlers take the structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send (dGame) instead of writing to Game::chatServer by hand. eChatChannel, eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs can use them. Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through both and requires identical bits, bytes and destination, checks the old readers read what the structs write, round trips every struct, checks truncated packets are refused, and has hand written golden packets for general chat, canned text, GM mute and a routed team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer zone flag made the tests fail. No wire bytes changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
644 lines
22 KiB
C++
644 lines
22 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);
|
|
}
|
|
|
|
ClientPackets::CharacterListResponse response;
|
|
response.selectedCharacterIndex = 0; //TODO: Pick the most recent played index. character index in front, just picking 0
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|