From ce528ee35bc87c761130f85bf34ce381a7814e5c Mon Sep 17 00:00:00 2001 From: WinterSolstice8 <60417494+wintersolstice8@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:53:42 -0600 Subject: [PATCH] [login] Adjust char info management during the lifetime of data_session This fixes a bug with multi-deletion or deletion->char creation Co-Authored-By: atom0s --- src/login/data_session.cpp | 225 ++++++++++++++++++++++-------------- src/login/data_session.h | 5 + src/login/login_helpers.cpp | 19 ++- src/login/login_helpers.h | 3 +- src/login/login_packets.h | 21 ++++ src/login/view_session.cpp | 22 +++- 6 files changed, 202 insertions(+), 93 deletions(-) diff --git a/src/login/data_session.cpp b/src/login/data_session.cpp index 0584a92a8ff..561a7bd58aa 100644 --- a/src/login/data_session.cpp +++ b/src/login/data_session.cpp @@ -25,6 +25,32 @@ #include "common/ipc.h" #include "common/utils.h" +void data_session::deleteCharFromCharInfo(uint32_t ffxi_id) +{ + for (auto& charInfo : characterInfoResponse.character_info) + { + if (ffxi_id == charInfo.ffxi_id) + { + charInfo.status = 0x01; // Available + charInfo.character_name[0] = 0x20; // space to display empty character slot, NULL displays a hume in a slot. + charInfo.character_name[1] = 0x00; // Null terminator so the client thinks the name is actually emptied. Otherwise it will display the deleted character. + } + } +} + +void data_session::addCharIntoCharInfo(const lpkt_chr_info_sub2& charInfo) +{ + // Find the first empty slot and fill it in. The client expects this. + for (auto& existingCharInfo : characterInfoResponse.character_info) + { + if (existingCharInfo.character_name[0] == 0x20) // empty - name is a space + { + existingCharInfo = charInfo; + break; + } + } +} + void data_session::read_func() { auto sessionHash = loginHelpers::getHashFromPacket(ipAddress, buffer_.data()); @@ -96,114 +122,137 @@ void data_session::read_func() return; } - lpkt_chr_info2 characterInfoResponse = {}; - characterInfoResponse.terminator = loginPackets::getTerminator(); - characterInfoResponse.command = 0x20; - loginPackets::clearIdentifier(characterInfoResponse); // server's name that shows in lobby menu const auto serverName = settings::get("main.SERVER_NAME"); char uList[500] = {}; - int i = 0; + uint32_t i = 0; - // Extract all the necessary information about each character from the database and load up the struct. - while (rset1->next()) + // Generate on first time read from db + if (!generatedCharInfo) { - char strCharName[16] = {}; // 15 characters + null terminator - std::memset(strCharName, 0, sizeof(strCharName)); + characterInfoResponse = {}; + characterInfoResponse.terminator = loginPackets::getTerminator(); + characterInfoResponse.command = 0x20; + loginPackets::clearIdentifier(characterInfoResponse); - std::string dbCharName = rset1->get("charname"); - std::memcpy(strCharName, dbCharName.c_str(), dbCharName.length()); - - int32 gmlevel = rset1->get("gmlevel"); - if (maintMode == 0 || gmlevel > 0) + // Extract all the necessary information about each character from the database and load up the struct. + while (rset1->next()) { - uint8 worldId = 0; // Use when multiple worlds are supported. - - uint32 charId = rset1->get("charid"); - uint32 contentId = charId; // Reusing the character ID as the content ID (which is also the name of character folder within the USER directory) at the moment - - // The character ID is made up of two parts totalling 24 bits: - uint16 charIdMain = charId & 0xFFFF; - uint8 charIdExtra = (charId >> 16) & 0xFF; - - auto& characterInfo = characterInfoResponse.character_info[i]; - - characterInfo.ffxi_id = contentId; - characterInfo.ffxi_id_world = charIdMain; - characterInfo.worldid = worldId; - characterInfo.status = 1; // 0 = Invalid/Hidden, 1 = Available, 2 = Disabled (unpaid) - characterInfo.race_change = 0; // 0 = no race change service, 1 = race change service (gold star icon) (NOT YET SUPPORTED!) - characterInfo.renamef = 0; // 0 = no rename required, 1 = rename required (NOT YET SUPPORTED!) - characterInfo.ffxi_id_world_tbl = charIdExtra; - - std::memcpy(characterInfo.character_name, &strCharName, 16); - std::memcpy(characterInfo.world_name, serverName.c_str(), std::clamp(serverName.length(), 0, 15)); - - uint16 zone = rset1->get("pos_zone"); - - uint8 MainJob = rset1->get("mjob"); - uint8 lvlMainJob = rset1->get(13 + MainJob); - - characterInfo.character_info.mon_no = rset1->get("race"); - characterInfo.character_info.mjob_no = MainJob; - characterInfo.character_info.mjob_level = lvlMainJob; - characterInfo.character_info.sjob_no = rset1->get("sjob"); - characterInfo.character_info.face_no = rset1->get("face"); // may not be calculated correctly? - characterInfo.character_info.town_no = rset1->get("nation"); - characterInfo.character_info.zone_no = static_cast(zone); - characterInfo.character_info.zone_no2 = static_cast((zone >> 8) & 1); - characterInfo.character_info.hair_no = rset1->get("face"); // may not be calculated correctly? - characterInfo.character_info.size = rset1->get("size"); - - // TODO: add check for DisplayHeadOffFlg - characterInfo.character_info.GrapIDTbl[0] = rset1->get("face"); // may not be calculated correctly? - characterInfo.character_info.GrapIDTbl[1] = rset1->get("head"); - characterInfo.character_info.GrapIDTbl[2] = rset1->get("body"); - characterInfo.character_info.GrapIDTbl[3] = rset1->get("hands"); - characterInfo.character_info.GrapIDTbl[4] = rset1->get("legs"); - characterInfo.character_info.GrapIDTbl[5] = rset1->get("feet"); - characterInfo.character_info.GrapIDTbl[6] = rset1->get("main"); - characterInfo.character_info.GrapIDTbl[7] = rset1->get("sub"); + char strCharName[16] = {}; // 15 characters + null terminator + std::memset(strCharName, 0, sizeof(strCharName)); - // uList is sent through data socket (to xiloader) - uint32 uListOffset = 16 * (i + 1); + std::string dbCharName = rset1->get("charname"); + std::memcpy(strCharName, dbCharName.c_str(), dbCharName.length()); + + int32 gmlevel = rset1->get("gmlevel"); + if (maintMode == 0 || gmlevel > 0) + { + uint8 worldId = 0; // Use when multiple worlds are supported. + + uint32 charId = rset1->get("charid"); + uint32 contentId = charId; // Reusing the character ID as the content ID (which is also the name of character folder within the USER directory) at the moment + + // The character ID is made up of two parts totalling 24 bits: + uint16 charIdMain = charId & 0xFFFF; + uint8 charIdExtra = (charId >> 16) & 0xFF; + + auto& characterInfo = characterInfoResponse.character_info[i]; + + characterInfo.ffxi_id = contentId; + characterInfo.ffxi_id_world = charIdMain; + characterInfo.worldid = worldId; + characterInfo.status = 1; // 0 = Invalid/Hidden, 1 = Available, 2 = Disabled (unpaid) + characterInfo.race_change = 0; // 0 = no race change service, 1 = race change service (gold star icon) (NOT YET SUPPORTED!) + characterInfo.renamef = 0; // 0 = no rename required, 1 = rename required (NOT YET SUPPORTED!) + characterInfo.ffxi_id_world_tbl = charIdExtra; + + std::memcpy(characterInfo.character_name, &strCharName, 16); + std::memcpy(characterInfo.world_name, serverName.c_str(), std::clamp(serverName.length(), 0, 15)); + + uint16 zone = rset1->get("pos_zone"); + + uint8 MainJob = rset1->get("mjob"); + uint8 lvlMainJob = rset1->get(13 + MainJob); + + characterInfo.character_info.mon_no = rset1->get("race"); + characterInfo.character_info.mjob_no = MainJob; + characterInfo.character_info.mjob_level = lvlMainJob; + characterInfo.character_info.sjob_no = rset1->get("sjob"); + characterInfo.character_info.face_no = rset1->get("face"); // may not be calculated correctly? + characterInfo.character_info.town_no = rset1->get("nation"); + characterInfo.character_info.zone_no = static_cast(zone); + characterInfo.character_info.zone_no2 = static_cast((zone >> 8) & 1); + characterInfo.character_info.hair_no = rset1->get("face"); // may not be calculated correctly? + characterInfo.character_info.size = rset1->get("size"); + + // TODO: add check for DisplayHeadOffFlg + characterInfo.character_info.GrapIDTbl[0] = rset1->get("face"); // may not be calculated correctly? + characterInfo.character_info.GrapIDTbl[1] = rset1->get("head"); + characterInfo.character_info.GrapIDTbl[2] = rset1->get("body"); + characterInfo.character_info.GrapIDTbl[3] = rset1->get("hands"); + characterInfo.character_info.GrapIDTbl[4] = rset1->get("legs"); + characterInfo.character_info.GrapIDTbl[5] = rset1->get("feet"); + characterInfo.character_info.GrapIDTbl[6] = rset1->get("main"); + characterInfo.character_info.GrapIDTbl[7] = rset1->get("sub"); + + // uList is sent through data socket (to xiloader) + uint32 uListOffset = 16 * (i + 1); + + ref(uList, uListOffset) = contentId; + ref(uList, uListOffset + 4) = charIdMain; + ref(uList, uListOffset + 6) = worldId; // Ignored in xiloader? + ref(uList, uListOffset + 7) = charIdExtra; // Ignored in xiloader? + + ++i; + characterInfoResponse.characters++; + } + } - ref(uList, uListOffset) = contentId; - ref(uList, uListOffset + 4) = charIdMain; - ref(uList, uListOffset + 6) = worldId; // Ignored in xiloader? - ref(uList, uListOffset + 7) = charIdExtra; // Ignored in xiloader? + generatedCharInfo = true; - ++i; - characterInfoResponse.characters++; + const auto allowCharacterCreation = settings::get("login.CHARACTER_CREATION"); + if (allowCharacterCreation) + { + // make extra char slots available if no characters are occupying the slots and their max content IDs supports it + while (characterInfoResponse.characters < numContentIds) + { + characterInfoResponse.character_info[characterInfoResponse.characters].status = 0x01; // Available + characterInfoResponse.character_info[characterInfoResponse.characters].character_name[0] = 0x20; // space to display empty character slot, NULL displays a hume in a slot. + characterInfoResponse.characters++; + } } - } - const auto allowCharacterCreation = settings::get("login.CHARACTER_CREATION"); - if (allowCharacterCreation) - { - // make extra char slots available if no characters are occupying the slots and their max content IDs supports it - while (characterInfoResponse.characters < numContentIds) + // the filtering above removes any non-GM characters so + // at this point we need to make sure stop players with empty lists + // from logging in or creating new characters + if (maintMode > 0 && i == 0) { - characterInfoResponse.character_info[characterInfoResponse.characters].status = 0x01; // Available - characterInfoResponse.character_info[characterInfoResponse.characters].character_name[0] = 0x20; // space to display empty character slot, NULL displays a hume in a slot. - characterInfoResponse.characters++; + if (auto viewSession = session.view_session.get()) + { + loginHelpers::generateErrorMessage(viewSession->buffer_.data(), loginErrors::errorCode::COULD_NOT_CONNECT_TO_LOBBY_SERVER); + viewSession->do_write(0x24); + } + ShowWarning(fmt::format("char:({}) attmpted login during maintenance mode (0xA2). Sending error to client.", session.accountID)); + return; } } - - // the filtering above removes any non-GM characters so - // at this point we need to make sure stop players with empty lists - // from logging in or creating new characters - if (maintMode > 0 && i == 0) + else { - if (auto viewSession = session.view_session.get()) + loginPackets::clearIdentifier(characterInfoResponse); + + for (i = 0; i < characterInfoResponse.characters; i++) { - loginHelpers::generateErrorMessage(viewSession->buffer_.data(), loginErrors::errorCode::COULD_NOT_CONNECT_TO_LOBBY_SERVER); - viewSession->do_write(0x24); + auto characterInfo = characterInfoResponse.character_info[i]; + // uList is sent through data socket (to xiloader) + uint32 uListOffset = 16 * (i + 1); + + ref(uList, uListOffset) = characterInfo.ffxi_id; // contentId + ref(uList, uListOffset + 4) = characterInfo.ffxi_id_world; // charIdMain + ref(uList, uListOffset + 6) = characterInfo.worldid; // Ignored in xiloader? + ref(uList, uListOffset + 7) = characterInfo.ffxi_id_world_tbl; // charIdExtra // Ignored in xiloader? } - ShowWarning(fmt::format("char:({}) attmpted login during maintenance mode (0xA2). Sending error to client.", session.accountID)); - return; } if (auto dataSession = session.data_session.get()) diff --git a/src/login/data_session.h b/src/login/data_session.h index 65c5a9ae993..c9657788672 100644 --- a/src/login/data_session.h +++ b/src/login/data_session.h @@ -43,6 +43,9 @@ class data_session : public handler_session DebugSockets("data_session from IP %s", ipAddress); } + void deleteCharFromCharInfo(uint32_t charid); + void addCharIntoCharInfo(const lpkt_chr_info_sub2& charInfo); + protected: void read_func() override; @@ -55,4 +58,6 @@ class data_session : public handler_session private: ZMQDealerWrapper& zmqDealerWrapper_; + lpkt_chr_info2 characterInfoResponse = {}; // Store this for char deletion/creation client behavior. We need to skip slots instead of "flatten" them. + bool generatedCharInfo = false; }; diff --git a/src/login/login_helpers.cpp b/src/login/login_helpers.cpp index 35f93e421d5..676404406c1 100644 --- a/src/login/login_helpers.cpp +++ b/src/login/login_helpers.cpp @@ -213,7 +213,7 @@ int32 saveCharacter(uint32 accid, uint32 charid, char_mini* createchar) return 0; } -int32 createCharacter(session_t& session, uint8* buf) +int32 createCharacter(session_t& session, uint8* buf, lpkt_chr_info_sub2& charInfo) { char_mini createchar{}; @@ -304,6 +304,23 @@ int32 createCharacter(session_t& session, uint8* buf) return -1; } + // The client expects to fill some data in on character creation. We never _see_ the character, so we don't need to set Race/Face/Model etc. + // We are making an assumption on what it wants - so for now just copy what is probably required (name, charid and some other stuff related to IDs.) + std::memcpy(&charInfo.character_name, charName.c_str(), std::min(charName.size(), sizeof(charInfo.character_name))); + + uint8 worldId = 0; // Use when multiple worlds are supported. + uint32 contentId = charID; // Reusing the character ID as the content ID (which is also the name of character folder within the USER directory) at the moment + uint16 charIdMain = charID & 0xFFFF; + uint8 charIdExtra = (charID >> 16) & 0xFF; + + charInfo.ffxi_id = contentId; + charInfo.ffxi_id_world = charIdMain; + charInfo.worldid = worldId; + charInfo.status = 1; // 0 = Invalid/Hidden, 1 = Available, 2 = Disabled (unpaid) + charInfo.race_change = 0; // 0 = no race change service, 1 = race change service (gold star icon) (NOT YET SUPPORTED!) + charInfo.renamef = 0; // 0 = no rename required, 1 = rename required (NOT YET SUPPORTED!) + charInfo.ffxi_id_world_tbl = charIdExtra; + ShowDebug(fmt::format("char <{}> successfully saved", charName)); return 0; } diff --git a/src/login/login_helpers.h b/src/login/login_helpers.h index 46f31f5d3c1..9868b3f9785 100644 --- a/src/login/login_helpers.h +++ b/src/login/login_helpers.h @@ -30,6 +30,7 @@ #include #include "login_errors.h" +#include "login_packets.h" #include "nlohmann/json.hpp" #include "session.h" @@ -97,7 +98,7 @@ uint16 generateFeatureBitmask(); int32 saveCharacter(uint32 accid, uint32 charid, char_mini* createchar); -int32 createCharacter(session_t& session, uint8* buf); +int32 createCharacter(session_t& session, uint8* buf, lpkt_chr_info_sub2& charInfo); std::string getHashFromPacket(const std::string& ip_str, uint8* data); diff --git a/src/login/login_packets.h b/src/login/login_packets.h index cb3eaef4f3e..3b6de554a11 100644 --- a/src/login/login_packets.h +++ b/src/login/login_packets.h @@ -166,3 +166,24 @@ struct lpkt_world_list : packet_t uint32_t sumofworld; // PS2: sumofworld lpkt_world_name world_name[1]; // PS2: world_name // size is 1 as we do not support multiple worlds yet. }; + +// PS2: lpkt_deletechr https://github.com/atom0s/XiPackets/blob/main/lobby/C2S_0x0014_RequestDeleteChr.md +struct lpkt_deletechr +{ + // + // Packet Header + // + + uint32_t packet_size; // PS2: packet_size + uint32_t terminator; // PS2: terminator + uint32_t command; // PS2: command + uint8_t identifer[16]; // PS2: identifer + + // + // Packet Data + // + + uint32_t ffxi_id; // PS2: ffxi_id + uint32_t ffxi_id_world; // PS2: ffxi_id_world + uint8_t passwd[16]; // PS2: passwd +}; diff --git a/src/login/view_session.cpp b/src/login/view_session.cpp index bd040baa8e9..d70e4ac260a 100644 --- a/src/login/view_session.cpp +++ b/src/login/view_session.cpp @@ -21,6 +21,8 @@ #include "view_session.h" +#include "data_session.h" + #include #include #include @@ -96,6 +98,9 @@ void view_session::read_func() return; } + lpkt_deletechr deleteCharPacket = {}; + std::memcpy(&deleteCharPacket, buffer_.data(), sizeof(lpkt_deletechr)); + std::memset(buffer_.data(), 0, 0x20); buffer_.data()[0] = 0x20; // size @@ -113,7 +118,7 @@ void view_session::read_func() do_write(0x20); - uint32 charID = ref(buffer_.data(), 0x20); + uint32 charID = deleteCharPacket.ffxi_id; ShowInfo(fmt::format("attempt to delete char:<{}> from ip:<{}>", charID, @@ -129,11 +134,16 @@ void view_session::read_func() if (accountID != session.accountID) { - ShowError(fmt::format("Account ID {} tried to delete character not in their account. (Note: there is a known issue that the client does not send the whole ID for characters above ID 65535 and this may not be their fault.)", session.accountID)); + ShowError(fmt::format("Account ID {} tried to delete character not in their account.", session.accountID)); socket_.lowest_layer().close(); return; } + if (auto data = dynamic_cast(session.data_session.get())) + { + data->deleteCharFromCharInfo(charID); + } + // Perform character deletion. // Instead of performing an actual character deletion, we simply set accid to 0, and original_accid to old accid. // This allows character recovery. @@ -149,13 +159,19 @@ void view_session::read_func() break; case 0x21: // 33: Registering character name onto the lobby server { + lpkt_chr_info_sub2 charInfo = {}; // creating new char - if (loginHelpers::createCharacter(session, buffer_.data()) == -1) + if (loginHelpers::createCharacter(session, buffer_.data(), charInfo) == -1) { socket_.lowest_layer().close(); return; } + if (auto data = dynamic_cast(session.data_session.get())) + { + data->addCharIntoCharInfo(charInfo); + } + session.justCreatedNewChar = true; ShowInfo(fmt::format("char <{}> was successfully created on account {}", session.requestedNewCharacterName, session.accountID));