diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 7e753a307..0d1292ac9 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -71,8 +71,6 @@ N(OP_BuyerItems), N(OP_CameraEffect), N(OP_Camp), N(OP_CancelSneakHide), -N(OP_CancelOfflineTrader), -N(OP_CancelOfflineTraderResponse), N(OP_CancelTask), N(OP_CancelTrade), N(OP_CashReward), diff --git a/common/servertalk.h b/common/servertalk.h index dd6d453c4..59357abc0 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -229,12 +229,12 @@ #define ServerOP_LSPlayerJoinWorld 0x3007 #define ServerOP_LSPlayerZoneChange 0x3008 -#define ServerOP_UsertoWorldReqLeg 0xAB00 -#define ServerOP_UsertoWorldRespLeg 0xAB01 -#define ServerOP_UsertoWorldReq 0xAB02 -#define ServerOP_UsertoWorldResp 0xAB03 -#define ServerOP_UsertoWorldCancelOfflineRequest 0xAB04 -#define ServerOP_UsertoWorldCancelOfflineResponse 0xAB05 +#define ServerOP_UsertoWorldReqLeg 0xAB00 +#define ServerOP_UsertoWorldRespLeg 0xAB01 +#define ServerOP_UsertoWorldReq 0xAB02 +#define ServerOP_UsertoWorldResp 0xAB03 +#define ServerOP_ReclaimOfflineSessionReq 0xAB04 +#define ServerOP_ReclaimOfflineSessionResp 0xAB05 #define ServerOP_LauncherConnectInfo 0x3000 #define ServerOP_LauncherZoneRequest 0x3001 @@ -367,8 +367,7 @@ enum { UserToWorldStatusSuspended = -1, UserToWorldStatusBanned = -2, UserToWorldStatusWorldAtCapacity = -3, - UserToWorldStatusAlreadyOnline = -4, - UserToWorldStatusOffilineTraderBuyer = -5 + UserToWorldStatusAlreadyOnline = -4 }; enum { @@ -380,6 +379,19 @@ enum { BazaarPurchaseBuyerSuccess = 5, BazaarPurchaseTraderFailed = 6 }; + +enum : uint8 { + OfflineSessionModeNone = 0, + OfflineSessionModeTrader = 1, + OfflineSessionModeBuyer = 2 +}; + +enum : int8 { + OfflineSessionReclaimFailed = 0, + OfflineSessionReclaimSuccess = 1, + OfflineSessionReclaimStale = 2, + OfflineSessionReclaimBusy = 3 +}; /************ PACKET RELATED STRUCT ************/ class ServerPacket { @@ -847,6 +859,18 @@ struct WorldToZone_Struct { uint32 account_id; int8 response; }; + +struct OfflineSessionReclaim_Struct { + uint32 request_id; + uint32 account_id; + uint32 character_id; + uint32 zone_id; + int32 instance_id; + uint32 entity_id; + uint8 mode; + int8 response; +}; + struct WorldShutDown_Struct { uint32 time; uint32 interval; diff --git a/docs/bazaar_item_unique_id_rollout.md b/docs/bazaar_item_unique_id_rollout.md index 3b9697dc2..ceccf5c14 100644 --- a/docs/bazaar_item_unique_id_rollout.md +++ b/docs/bazaar_item_unique_id_rollout.md @@ -1,9 +1,11 @@ -# Bazaar Item Unique ID And Offline Trading Rollout +# Bazaar Item Unique ID And World-Only Offline Trading Rollout ## Purpose This rollout converts persisted item identity and offline trader or buyer session state to the new production-safe model. The migration is designed for a maintenance window and explicitly clears any in-flight trader, buyer, and offline sessions during cutover. +Offline trader and buyer reconnect now use the normal world and zone flow. No custom loginserver support is required. World checks for an indexed `offline_character_sessions` row during character entry and only pauses login when it must reclaim an active offline trader or buyer session. + Do not reopen the server until every verification step passes. ## Preconditions @@ -11,6 +13,8 @@ Do not reopen the server until every verification step passes. - Schedule a maintenance window. - Stop new logins before beginning the migration. - Ensure the `world` binary you are deploying includes this branch. +- Ensure the deployed `world` and `zone` binaries are from the same build. +- A stock or public loginserver is supported; no custom loginserver rollout is required. - Ensure operators have credentials to run schema updates and database dump commands. ## Mandatory Backup @@ -40,6 +44,9 @@ Validate these gameplay scenarios after the migration: - One trader changing a price without affecting another trader. - Offline trader purchase. - Offline buyer purchase. +- Relog to the same account while an offline trader is active and verify entry resumes after reclaim. +- Log into a different character on the same account while an offline buyer is active and verify the offline session is ended before entry continues. +- Confirm an unresponsive reclaim path fails the character-entry attempt within 10 seconds. - Parcel retrieval for rows that previously had missing `item_unique_id` values. - Alternate bazaar shard search. @@ -76,6 +83,13 @@ world database:item-unique-ids --verify --verbose 8. Reopen the server only after verification passes. +## Login Performance Expectation + +- Character entry with no offline session should take the normal fast path and only add one indexed lookup by `account_id`. +- Character entry with an offline trader or buyer session should target exactly one owning zone or instance. +- If the owning zone is down, world clears the stale session locally and continues immediately. +- If the owning zone does not answer a reclaim request, world fails the character-entry attempt after 10 seconds instead of hanging indefinitely. + ## What Preflight And Verify Must Show The migration is not complete unless all of the following are true: diff --git a/loginserver/client.cpp b/loginserver/client.cpp index 20fbca94d..754c83ab5 100644 --- a/loginserver/client.cpp +++ b/loginserver/client.cpp @@ -71,25 +71,6 @@ bool Client::Process() SendPlayToWorld((const char *) app->pBuffer); break; } - case OP_CancelOfflineTrader: { - if (app->Size() < sizeof(CancelOfflineTrader)) { - LogError("Play received but it is too small, discarding"); - break; - } - - safe_delete_array(app->pBuffer); - auto buffer = new unsigned char[sizeof(PlayEverquestRequest)]; - auto data = (PlayEverquestRequest *) buffer; - data->base_header.sequence = GetCurrentPlaySequence(); - data->server_number = GetSelectedPlayServerID(); - app->pBuffer = buffer; - app->size = sizeof(PlayEverquestRequest); - - LogLoginserverDetail("Step 1 - Hit CancelOfflineTrader Mode Packet for."); - SendCancelOfflineStatusToWorld((const char *) app->pBuffer); - - break; - } } delete app; @@ -580,27 +561,3 @@ std::string Client::GetClientLoggingDescription() client_ip ); } - -void Client::SendCancelOfflineStatusToWorld(const char *data) -{ - if (m_client_status != cs_logged_in) { - LogError("Client sent a play request when they were not logged in, discarding"); - return; - } - - const auto *play = (const PlayEverquestRequest *) data; - auto server_id_in = (unsigned int) play->server_number; - auto sequence_in = (unsigned int) play->base_header.sequence; - - LogLoginserverDetail( - "Step 2 - Cancel Offline Status Request received from client [{}] server number [{}] sequence [{}]", - GetAccountName(), - server_id_in, - sequence_in - ); - - m_selected_play_server_id = (unsigned int) play->server_number; - m_play_sequence_id = sequence_in; - m_selected_play_server_id = server_id_in; - server.server_manager->SendUserToWorldCancelOfflineRequest(server_id_in, m_account_id, m_loginserver_name); -} diff --git a/loginserver/client.h b/loginserver/client.h index a8f5d6cce..5baa4eebe 100644 --- a/loginserver/client.h +++ b/loginserver/client.h @@ -25,7 +25,6 @@ public: void SendPlayToWorld(const char *data); void SendServerListPacket(uint32 seq); void SendPlayResponse(EQApplicationPacket *outapp); - void SendCancelOfflineStatusToWorld(const char *data); void GenerateRandomLoginKey(); unsigned int GetAccountID() const { return m_account_id; } std::string GetLoginServerName() const { return m_loginserver_name; } diff --git a/loginserver/login_types.h b/loginserver/login_types.h index 01fa26c74..b760eb7fe 100644 --- a/loginserver/login_types.h +++ b/loginserver/login_types.h @@ -83,11 +83,6 @@ struct PlayEverquestResponse { uint32 server_number; }; -struct CancelOfflineTrader { - LoginBaseMessage base_header; - int16_t unk; -}; - #pragma pack() enum LSClientVersion { @@ -163,12 +158,11 @@ namespace LS { constexpr static int ERROR_NONE = 101; // No Error constexpr static int ERROR_UNKNOWN = 102; // Error - Unknown Error Occurred constexpr static int ERROR_ACTIVE_CHARACTER = 111; // Error 1018: You currently have an active character on that EverQuest Server, please allow a minute for synchronization and try again. - constexpr static int ERROR_OFFLINE_TRADER = 114; // You have a character logged into a world server as an OFFLINE TRADER from this account. You may only have 1 character from a single account logged into a server at a time (even across different servers). Would you like to remove this character from the game so you may login? constexpr static int ERROR_SERVER_UNAVAILABLE = 326; // That server is currently unavailable. Please check the EverQuest webpage for current server status and try again later. constexpr static int ERROR_ACCOUNT_SUSPENDED = 337; // This account is currently suspended. Please contact customer service for more information. constexpr static int ERROR_ACCOUNT_BANNED = 338; // This account is currently banned. Please contact customer service for more information. constexpr static int ERROR_WORLD_MAX_CAPACITY = 339; // The world server is currently at maximum capacity and not allowing further logins until the number of players online decreases. Please try again later. - }; + }; } #pragma pack(pop) diff --git a/loginserver/login_util/login_opcodes_sod.conf b/loginserver/login_util/login_opcodes_sod.conf index 9a21a8dcc..cdc856d2c 100644 --- a/loginserver/login_util/login_opcodes_sod.conf +++ b/loginserver/login_util/login_opcodes_sod.conf @@ -11,5 +11,3 @@ OP_Poll=0x0029 OP_LoginExpansionPacketData=0x0031 OP_EnterChat=0x000f OP_PollResponse=0x0011 -OP_CancelOfflineTrader=0x0016 -OP_CancelOfflineTraderResponse=0x0030 diff --git a/loginserver/world_server.cpp b/loginserver/world_server.cpp index 4187f7043..85b9d6c74 100644 --- a/loginserver/world_server.cpp +++ b/loginserver/world_server.cpp @@ -52,12 +52,6 @@ WorldServer::WorldServer(std::shared_ptr wo ServerOP_LSAccountUpdate, std::bind(&WorldServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2) ); - - worldserver_connection->OnMessage( - ServerOP_UsertoWorldCancelOfflineResponse, - std::bind( - &WorldServer::ProcessUserToWorldCancelOfflineResponse, this, std::placeholders::_1, std::placeholders::_2) - ); } WorldServer::~WorldServer() = default; @@ -305,10 +299,6 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac case UserToWorldStatusAlreadyOnline: r->base_reply.error_str_id = LS::ErrStr::ERROR_ACTIVE_CHARACTER; break; - case UserToWorldStatusOffilineTraderBuyer: - r->base_reply.success = false; - r->base_reply.error_str_id = LS::ErrStr::ERROR_OFFLINE_TRADER; - break; default: r->base_reply.error_str_id = LS::ErrStr::ERROR_UNKNOWN; break; @@ -785,113 +775,3 @@ void WorldServer::FormatWorldServerName(char *name, int8 server_list_type) strn0cpy(name, server_long_name.c_str(), 201); } - -void WorldServer::ProcessUserToWorldCancelOfflineResponse(uint16_t opcode, const EQ::Net::Packet &packet) - { - LogNetcode( - "Application packet received from server [{:#04x}] [Size: {}]\n{}", - opcode, - packet.Length(), - packet.ToString() - ); - LogLoginserverDetail("Step 8 - back in Login Server from world."); - - if (packet.Length() < sizeof(UsertoWorldResponse)) { - LogError( - "Received application packet from server that had opcode ServerOP_UsertoWorldCancelOfflineResp, " - "but was too small. Discarded to avoid buffer overrun" - ); - return; - } - - auto res = (UsertoWorldResponse *) packet.Data(); - LogDebug("Trying to find client with user id of [{}]", res->lsaccountid); - - Client *c = server.client_manager->GetClient( - res->lsaccountid, - res->login - ); - - if (c) { - LogDebug( - "Found client with user id of [{}] and account name of {}", - res->lsaccountid, - c->GetAccountName().c_str() - ); - - auto client_packet = EQApplicationPacket(OP_CancelOfflineTraderResponse, sizeof(PlayEverquestResponse)); - auto client_packet_payload = reinterpret_cast(client_packet.pBuffer); - - client_packet_payload->base_header.sequence = c->GetCurrentPlaySequence(); - client_packet_payload->server_number = c->GetSelectedPlayServerID(); - - LogLoginserverDetail( - "Step 9 - Send Play Response OPCODE 30 to remove the client message about having an offline Trader/Buyer" - ); - c->SendPlayResponse(&client_packet); - - auto outapp = new EQApplicationPacket(OP_PlayEverquestResponse, sizeof(PlayEverquestResponse)); - auto r = reinterpret_cast(outapp->pBuffer); - r->base_header.sequence = c->GetCurrentPlaySequence(); - r->server_number = c->GetSelectedPlayServerID(); - - LogDebug( - "Found sequence and play of [{}] [{}]", - c->GetCurrentPlaySequence(), - c->GetSelectedPlayServerID() - ); - - //LogDebug("[Size: [{}]] {}", outapp->size, DumpPacketToString(outapp)); - - if (res->response > 0) { - r->base_reply.success = true; - SendClientAuthToWorld(c); - } - - switch (res->response) { - case UserToWorldStatusSuccess: - r->base_reply.error_str_id = LS::ErrStr::ERROR_NONE; - break; - case UserToWorldStatusWorldUnavail: - r->base_reply.error_str_id = LS::ErrStr::ERROR_SERVER_UNAVAILABLE; - break; - case UserToWorldStatusSuspended: - r->base_reply.error_str_id = LS::ErrStr::ERROR_ACCOUNT_SUSPENDED; - break; - case UserToWorldStatusBanned: - r->base_reply.error_str_id = LS::ErrStr::ERROR_ACCOUNT_BANNED; - break; - case UserToWorldStatusWorldAtCapacity: - r->base_reply.error_str_id = LS::ErrStr::ERROR_WORLD_MAX_CAPACITY; - break; - case UserToWorldStatusAlreadyOnline: - r->base_reply.error_str_id = LS::ErrStr::ERROR_ACTIVE_CHARACTER; - break; - case UserToWorldStatusOffilineTraderBuyer: - r->base_reply.success = false; - r->base_reply.error_str_id = LS::ErrStr::ERROR_OFFLINE_TRADER; - break; - default: - r->base_reply.error_str_id = LS::ErrStr::ERROR_UNKNOWN; - break; - } - - LogDebug( - "Sending play response with following data, allowed [{}], sequence {}, server number {}, message {}", - r->base_reply.success, - r->base_header.sequence, - r->server_number, - r->base_reply.error_str_id - ); - LogLoginserverDetail("Step 10 - Send Play Response EnterWorld to client"); - - c->SendPlayResponse(outapp); - delete outapp; - } - else { - LogError( - "Received User-To-World Response for [{}] but could not find the client referenced!.", - res->lsaccountid - ); - } - } \ No newline at end of file diff --git a/loginserver/world_server.h b/loginserver/world_server.h index f48f4f085..94ed4b4a7 100644 --- a/loginserver/world_server.h +++ b/loginserver/world_server.h @@ -58,7 +58,6 @@ private: void ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Net::Packet &packet); void ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Packet &packet); void ProcessLSAccountUpdate(uint16_t opcode, const EQ::Net::Packet &packet); - void ProcessUserToWorldCancelOfflineResponse(uint16_t opcode, const EQ::Net::Packet &packet); std::shared_ptr m_connection; diff --git a/loginserver/world_server_manager.cpp b/loginserver/world_server_manager.cpp index bbe73ddd4..fdf917e7e 100644 --- a/loginserver/world_server_manager.cpp +++ b/loginserver/world_server_manager.cpp @@ -216,35 +216,3 @@ const std::list> &WorldServerManager::GetWorldServe { return m_world_servers; } - -void WorldServerManager::SendUserToWorldCancelOfflineRequest( - unsigned int server_id, - unsigned int client_account_id, - const std::string &client_loginserver -) -{ - auto iter = std::find_if( - m_world_servers.begin(), m_world_servers.end(), - [&](const std::unique_ptr &server) { - return server->GetServerId() == server_id; - } - ); - - if (iter != m_world_servers.end()) { - EQ::Net::DynamicPacket outapp; - outapp.Resize(sizeof(UsertoWorldRequest)); - - auto *r = reinterpret_cast(outapp.Data()); - r->worldid = server_id; - r->lsaccountid = client_account_id; - strncpy(r->login, client_loginserver.c_str(), 64); - - LogLoginserverDetail("Step 3 - Sending ServerOP_UsertoWorldCancelOfflineRequest to world for client account id {}", client_account_id); - (*iter)->GetConnection()->Send(ServerOP_UsertoWorldCancelOfflineRequest, outapp); - - LogNetcode("[UsertoWorldRequest] [Size: {}]\n{}", outapp.Length(), outapp.ToString()); - } - else { - LogError("Client requested a user to world but supplied an invalid id of {}", server_id); - } -} \ No newline at end of file diff --git a/loginserver/world_server_manager.h b/loginserver/world_server_manager.h index 20f4df309..1885b1669 100644 --- a/loginserver/world_server_manager.h +++ b/loginserver/world_server_manager.h @@ -16,11 +16,6 @@ public: unsigned int client_account_id, const std::string &client_loginserver ); - void SendUserToWorldCancelOfflineRequest( - unsigned int server_id, - unsigned int client_account_id, - const std::string &client_loginserver - ); std::unique_ptr CreateServerListPacket(Client *client, uint32 sequence); bool DoesServerExist(const std::string &s, const std::string &server_short_name, WorldServer *ignore = nullptr); void DestroyServerByName(std::string s, std::string server_short_name, WorldServer *ignore = nullptr); diff --git a/world/client.cpp b/world/client.cpp index e65b6d801..ea241f297 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -37,10 +37,13 @@ #include "common/races.h" #include "common/random.h" #include "common/repositories/account_repository.h" +#include "common/repositories/buyer_repository.h" #include "common/repositories/character_data_repository.h" #include "common/repositories/group_id_repository.h" #include "common/repositories/inventory_repository.h" +#include "common/repositories/offline_character_sessions_repository.h" #include "common/repositories/player_event_logs_repository.h" +#include "common/repositories/trader_repository.h" #include "common/rulesys.h" #include "common/shareddb.h" #include "common/skill_caps.h" @@ -59,6 +62,7 @@ #include "zlib.h" #include +#include #include #include #include @@ -88,6 +92,60 @@ extern uint32 numclients; extern volatile bool RunLoops; extern volatile bool UCSServerAvailable_; +namespace { +constexpr uint32 kOfflineSessionReclaimTimeoutMs = 10000; +std::atomic g_offline_reclaim_request_id{1}; + +uint8 ToOfflineSessionMode(const std::string &mode) +{ + if (Strings::EqualFold(mode, "buyer")) { + return OfflineSessionModeBuyer; + } + + if (Strings::EqualFold(mode, "trader")) { + return OfflineSessionModeTrader; + } + + return OfflineSessionModeNone; +} + +const char *OfflineSessionModeName(uint8 mode) +{ + switch (mode) { + case OfflineSessionModeTrader: + return "trader"; + case OfflineSessionModeBuyer: + return "buyer"; + default: + return "unknown"; + } +} + +const char *OfflineSessionReclaimResponseName(int8 response) +{ + switch (response) { + case OfflineSessionReclaimSuccess: + return "success"; + case OfflineSessionReclaimStale: + return "stale"; + case OfflineSessionReclaimBusy: + return "busy"; + default: + return "failed"; + } +} + +uint32 NextOfflineReclaimRequestId() +{ + auto request_id = g_offline_reclaim_request_id.fetch_add(1); + if (request_id == 0) { + request_id = g_offline_reclaim_request_id.fetch_add(1); + } + + return request_id; +} +} + // unused ATM, but here for reference, should match RoF2 enum class NameApprovalResponse : int { NotValid = -1, // string ID 1576 @@ -102,6 +160,7 @@ enum class NameApprovalResponse : int { Client::Client(EQStreamInterface* ieqs) : autobootup_timeout(RuleI(World, ZoneAutobootTimeoutMS)), connect(1000), + offline_reclaim_timeout(kOfflineSessionReclaimTimeoutMs), eqs(ieqs) { // Live does not send datarate as of 3/11/2005 @@ -111,6 +170,7 @@ Client::Client(EQStreamInterface* ieqs) autobootup_timeout.Disable(); connect.Disable(); + offline_reclaim_timeout.Disable(); seen_character_select = false; cle = 0; zone_id = 0; @@ -119,6 +179,14 @@ Client::Client(EQStreamInterface* ieqs) zone_waiting_for_bootup = 0; enter_world_triggered = false; StartInTutorial = false; + offline_reclaim_pending = false; + offline_reclaim_request_id = 0; + offline_reclaim_character_id = 0; + offline_reclaim_zone_id = 0; + offline_reclaim_instance_id = 0; + offline_reclaim_entity_id = 0; + offline_reclaim_started_at = 0; + offline_reclaim_mode = OfflineSessionModeNone; m_ClientVersion = eqs->ClientVersion(); m_ClientVersionBit = EQ::versions::ConvertClientVersionToClientVersionBit(m_ClientVersion); @@ -932,6 +1000,17 @@ bool Client::HandleEnterWorldPacket(const EQApplicationPacket *app) { } } + if (!BeginOfflineSessionReclaimIfNeeded()) { + return true; + } + + ContinueEnterWorld(); + + return true; +} + +void Client::ContinueEnterWorld() +{ if(!is_player_zoning) { GroupIdRepository::DeleteWhere( database, @@ -1066,10 +1145,201 @@ bool Client::HandleEnterWorldPacket(const EQApplicationPacket *app) { } EnterWorld(); +} + +bool Client::BeginOfflineSessionReclaimIfNeeded() +{ + if (offline_reclaim_pending) { + return false; + } + + const auto session = OfflineCharacterSessionsRepository::GetByAccountId(database, GetAccountID()); + if (!session.id) { + return true; + } + + const auto mode = ToOfflineSessionMode(session.mode); + if (mode == OfflineSessionModeNone) { + LogWarning( + "Character entry for [{}] account [{}] found offline session with unexpected mode [{}]; attempting targeted reclaim using stored zone and entity metadata", + GetCharName(), + GetAccountID(), + session.mode + ); + } + + LogInfo( + "Character entry for [{}] account [{}] found offline {} session character_id [{}] zone_id [{}] instance_id [{}] entity_id [{}]", + GetCharName(), + GetAccountID(), + OfflineSessionModeName(mode), + session.character_id, + session.zone_id, + session.instance_id, + session.entity_id + ); + + auto zone_server = session.instance_id > 0 ? + ZSList::Instance()->FindByInstanceID(session.instance_id) : + ZSList::Instance()->FindByZoneID(session.zone_id); + + if (!zone_server || !zone_server->IsConnected()) { + auto clear_started_at = Timer::GetCurrentTime(); + if (!ClearStaleOfflineSession(session.character_id, "zone not booted")) { + LogError( + "Failed clearing stale offline {} session locally for account [{}] character [{}]", + OfflineSessionModeName(mode), + GetAccountID(), + session.character_id + ); + TellClientZoneUnavailable(); + return false; + } + + LogInfo( + "Cleared stale offline {} session locally for account [{}] character [{}] in [{}] ms", + OfflineSessionModeName(mode), + GetAccountID(), + session.character_id, + Timer::GetCurrentTime() - clear_started_at + ); + return true; + } + + auto pack = new ServerPacket(ServerOP_ReclaimOfflineSessionReq, sizeof(OfflineSessionReclaim_Struct)); + auto reclaim = reinterpret_cast(pack->pBuffer); + memset(pack->pBuffer, 0, pack->size); + + offline_reclaim_pending = true; + offline_reclaim_request_id = NextOfflineReclaimRequestId(); + offline_reclaim_character_id = session.character_id; + offline_reclaim_zone_id = session.zone_id; + offline_reclaim_instance_id = session.instance_id; + offline_reclaim_entity_id = session.entity_id; + offline_reclaim_started_at = Timer::GetCurrentTime(); + offline_reclaim_mode = mode; + offline_reclaim_timeout.Start(kOfflineSessionReclaimTimeoutMs); + + reclaim->request_id = offline_reclaim_request_id; + reclaim->account_id = GetAccountID(); + reclaim->character_id = session.character_id; + reclaim->zone_id = session.zone_id; + reclaim->instance_id = session.instance_id; + reclaim->entity_id = session.entity_id; + reclaim->mode = mode; + reclaim->response = OfflineSessionReclaimFailed; + + LogInfo( + "Sending targeted offline {} reclaim request [{}] to zone [{}] instance [{}] for account [{}]", + OfflineSessionModeName(mode), + offline_reclaim_request_id, + session.zone_id, + session.instance_id, + GetAccountID() + ); + + zone_server->SendPacket(pack); + safe_delete(pack); + return false; +} + +bool Client::ClearStaleOfflineSession(uint32 character_id, const char *reason) +{ + database.TransactionBegin(); + AccountRepository::SetOfflineStatus(database, GetAccountID(), false); + OfflineCharacterSessionsRepository::DeleteByAccountId(database, GetAccountID()); + + if (character_id) { + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = {}", character_id)); + BuyerRepository::DeleteBuyer(database, character_id); + } + + auto commit_result = database.TransactionCommit(); + if (!commit_result.Success()) { + database.TransactionRollback(); + LogError( + "Failed clearing stale offline session for account [{}] character [{}] while {}: ({}) {}", + GetAccountID(), + character_id, + reason ? reason : "processing entry", + commit_result.ErrorNumber(), + commit_result.ErrorMessage() + ); + return false; + } return true; } +void Client::ResetOfflineSessionReclaimState() +{ + offline_reclaim_timeout.Disable(); + offline_reclaim_pending = false; + offline_reclaim_request_id = 0; + offline_reclaim_character_id = 0; + offline_reclaim_zone_id = 0; + offline_reclaim_instance_id = 0; + offline_reclaim_entity_id = 0; + offline_reclaim_started_at = 0; + offline_reclaim_mode = OfflineSessionModeNone; +} + +void Client::HandleOfflineSessionReclaimResponse(const OfflineSessionReclaim_Struct &response) +{ + if (!offline_reclaim_pending) { + LogInfo( + "Ignoring offline reclaim response [{}] for account [{}] because no reclaim is pending", + response.request_id, + response.account_id + ); + return; + } + + if (response.request_id != offline_reclaim_request_id) { + LogInfo( + "Ignoring stale offline reclaim response [{}] for account [{}]; current request is [{}]", + response.request_id, + response.account_id, + offline_reclaim_request_id + ); + return; + } + + auto elapsed_ms = Timer::GetCurrentTime() - offline_reclaim_started_at; + + LogInfo( + "Received offline {} reclaim response [{}] status [{}] after [{}] ms for account [{}]", + OfflineSessionModeName(offline_reclaim_mode), + response.request_id, + OfflineSessionReclaimResponseName(response.response), + elapsed_ms, + response.account_id + ); + + if (response.response == OfflineSessionReclaimSuccess) { + ResetOfflineSessionReclaimState(); + ContinueEnterWorld(); + return; + } + + if (response.response == OfflineSessionReclaimStale) { + auto clear_started_at = Timer::GetCurrentTime(); + if (ClearStaleOfflineSession(offline_reclaim_character_id, "zone confirmed stale session")) { + LogInfo( + "Cleared stale offline {} session for account [{}] after zone confirmation in [{}] ms", + OfflineSessionModeName(offline_reclaim_mode), + GetAccountID(), + Timer::GetCurrentTime() - clear_started_at + ); + ResetOfflineSessionReclaimState(); + ContinueEnterWorld(); + return; + } + } + + TellClientZoneUnavailable(); +} + bool Client::HandleDeleteCharacterPacket(const EQApplicationPacket *app) { uint32 char_acct_id = database.GetAccountIDByChar((char*)app->pBuffer); @@ -1223,6 +1493,18 @@ bool Client::Process() { TellClientZoneUnavailable(); } + if (offline_reclaim_pending && offline_reclaim_timeout.Check()) { + auto elapsed_ms = Timer::GetCurrentTime() - offline_reclaim_started_at; + LogWarning( + "Offline {} reclaim timed out after [{}] ms for account [{}] selected character [{}]", + OfflineSessionModeName(offline_reclaim_mode), + elapsed_ms, + GetAccountID(), + GetCharName() + ); + TellClientZoneUnavailable(); + } + if(connect.Check()){ SendGuildList();// Send OPCode: OP_GuildsList SendApproveWorld(); @@ -1601,6 +1883,7 @@ void Client::TellClientZoneUnavailable() { zone_waiting_for_bootup = 0; enter_world_triggered = false; autobootup_timeout.Disable(); + ResetOfflineSessionReclaimState(); } void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req) { diff --git a/world/client.h b/world/client.h index f26dcaaac..72e05bc12 100644 --- a/world/client.h +++ b/world/client.h @@ -51,6 +51,7 @@ public: void SendPostEnterWorld(); void SendGuildTributeFavorAndTimer(uint32 favor, uint32 time_remaining); void SendGuildTributeOptInToggle(const GuildTributeMemberToggle* in); + void HandleOfflineSessionReclaimResponse(const OfflineSessionReclaim_Struct &response); inline uint32 GetIP() { return ip; } inline uint16 GetPort() { return port; } @@ -100,6 +101,15 @@ private: ClientListEntry* cle; Timer connect; bool seen_character_select; + Timer offline_reclaim_timeout; + bool offline_reclaim_pending; + uint32 offline_reclaim_request_id; + uint32 offline_reclaim_character_id; + uint32 offline_reclaim_zone_id; + int32 offline_reclaim_instance_id; + uint32 offline_reclaim_entity_id; + uint32 offline_reclaim_started_at; + uint8 offline_reclaim_mode; bool HandlePacket(const EQApplicationPacket *app); bool HandleNameApprovalPacket(const EQApplicationPacket *app); @@ -114,6 +124,10 @@ private: bool ChecksumVerificationCRCEQGame(uint64 checksum); bool ChecksumVerificationCRCSkillCaps(uint64 checksum); bool ChecksumVerificationCRCBaseData(uint64 checksum); + void ContinueEnterWorld(); + bool BeginOfflineSessionReclaimIfNeeded(); + bool ClearStaleOfflineSession(uint32 character_id, const char *reason); + void ResetOfflineSessionReclaimState(); EQStreamInterface* eqs; bool CanTradeFVNoDropItem(); diff --git a/world/login_server.cpp b/world/login_server.cpp index 0554fd4bb..969fe9525 100644 --- a/world/login_server.cpp +++ b/world/login_server.cpp @@ -4,11 +4,6 @@ #include "common/eqemu_logsys.h" #include "common/misc_functions.h" #include "common/packet_dump.h" -#include "common/repositories/account_repository.h" -#include "common/repositories/buyer_repository.h" -#include "common/repositories/character_data_repository.h" -#include "common/repositories/offline_character_sessions_repository.h" -#include "common/repositories/trader_repository.h" #include "common/servertalk.h" #include "common/strings.h" #include "common/version.h" @@ -195,13 +190,6 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet &p) return; } - if (status_record.offline || OfflineCharacterSessionsRepository::ExistsByAccountId(database, id)) { - LogDebug("User has an offline character for account_id [{0}]", utwr->lsaccountid); - utwrs->response = UserToWorldStatusOffilineTraderBuyer; - SendPacket(&outpack); - return; - } - if (RuleB(World, EnforceCharacterLimitAtLogin)) { if (ClientList::Instance()->IsAccountInGame(utwr->lsaccountid)) { LogDebug("User already online account_id [{0}]", utwr->lsaccountid); @@ -589,14 +577,6 @@ bool LoginServer::Connect() std::placeholders::_2 ) ); - m_client->OnMessage( - ServerOP_UsertoWorldCancelOfflineRequest, - std::bind( - &LoginServer::ProcessUserToWorldCancelOfflineRequest, - this, - std::placeholders::_1, - std::placeholders::_2) - ); } return true; @@ -711,109 +691,3 @@ void LoginServer::SendAccountUpdate(ServerPacket *pack) SendPacket(pack); } } - -void LoginServer::ProcessUserToWorldCancelOfflineRequest(uint16_t opcode, EQ::Net::Packet &p) -{ - auto const Config = WorldConfig::get(); - LogNetcode("Received ServerPacket from LS OpCode {:#04x}", opcode); - - auto utwr = static_cast(p.Data()); - uint32 id = database.GetAccountIDFromLSID(utwr->login, utwr->lsaccountid); - auto status_record = database.GetAccountStatus(id); - - LogLoginserverDetail( - "Step 4 - World received CancelOfflineRequest for client login server account id {} offline mode {}", - id, - status_record.offline - ); - LogDebug( - "id [{}] status [{}] account_id [{}] world_id [{}] ip [{}]", - id, - status_record.status, - utwr->lsaccountid, - utwr->worldid, - utwr->IPAddr - ); - - ServerPacket server_packet; - server_packet.size = sizeof(UsertoWorldResponse); - server_packet.pBuffer = new uchar[server_packet.size]; - memset(server_packet.pBuffer, 0, server_packet.size); - - auto utwrs = reinterpret_cast(server_packet.pBuffer); - utwrs->lsaccountid = utwr->lsaccountid; - utwrs->ToID = utwr->FromID; - utwrs->worldid = utwr->worldid; - utwrs->response = UserToWorldStatusSuccess; - strn0cpy(utwrs->login, utwr->login, 64); - - if (Config->Locked == true) { - if (status_record.status < RuleI(GM, MinStatusToBypassLockedServer)) { - LogDebug("Server locked and status is not high enough for account_id [{0}]", utwr->lsaccountid); - server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; - utwrs->response = UserToWorldStatusWorldUnavail; - SendPacket(&server_packet); - return; - } - } - - int32 x = Config->MaxClients; - if (static_cast(numplayers) >= x && - x != -1 && - x != 255 && - status_record.status < RuleI(GM, MinStatusToBypassLockedServer) - ) { - LogDebug("World at capacity account_id [{0}]", utwr->lsaccountid); - server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; - utwrs->response = UserToWorldStatusWorldAtCapacity; - SendPacket(&server_packet); - return; - } - - auto session = OfflineCharacterSessionsRepository::GetByAccountId(database, id); - auto trader = TraderRepository::GetAccountZoneIdAndInstanceIdByAccountId(database, id); - uint32 zone_id = session.id ? session.zone_id : trader.char_zone_id; - int32 instance_id = session.id ? session.instance_id : trader.char_zone_instance_id; - uint32 character_id = session.id ? session.character_id : trader.character_id; - - if ((session.id || trader.id) && - ZSList::Instance()->IsZoneBootedByZoneIdAndInstanceId(zone_id, instance_id)) { - LogLoginserverDetail( - "Step 5a(1) - World Checked offline users zone/instance is booted. " - "Sending packet to zone id {} instance id {}", - zone_id, - instance_id); - - server_packet.opcode = ServerOP_UsertoWorldCancelOfflineRequest; - ZSList::Instance()->SendPacketToBootedZones(&server_packet); - return; - } - - LogLoginserverDetail("Step 5b(1) - World determined offline users zone/instance is not booted. Ignoring zone."); - - LogLoginserverDetail("Step 5b(2) - World clearing users offline status from account table."); - database.TransactionBegin(); - AccountRepository::SetOfflineStatus(database, id, false); - OfflineCharacterSessionsRepository::DeleteByAccountId(database, id); - - LogLoginserverDetail("Step 5b(3) - World clearing trader and buyer tables."); - if (character_id) { - TraderRepository::DeleteWhere(database, fmt::format("`character_id` = '{}'", character_id)); - BuyerRepository::DeleteBuyer(database, character_id); - } - - auto commit_result = database.TransactionCommit(); - if (!commit_result.Success()) { - database.TransactionRollback(); - LogError( - "Failed clearing offline session state for account [{}]: ({}) {}", - id, - commit_result.ErrorNumber(), - commit_result.ErrorMessage() - ); - utwrs->response = UserToWorldStatusWorldUnavail; - } - - server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; - SendPacket(&server_packet); -} diff --git a/world/login_server.h b/world/login_server.h index c2d2f2022..e77366411 100644 --- a/world/login_server.h +++ b/world/login_server.h @@ -49,7 +49,6 @@ private: void ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet &p); void ProcessLSRemoteAddr(uint16_t opcode, EQ::Net::Packet &p); void ProcessLSAccountUpdate(uint16_t opcode, EQ::Net::Packet &p); - void ProcessUserToWorldCancelOfflineRequest(uint16_t opcode, EQ::Net::Packet &p); std::unique_ptr m_keepalive; diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 66e11d815..d6a0dbbc0 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -779,6 +779,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { if (client) { client->Clearance(wtz->response); } + break; } case ServerOP_ZoneToZoneRequest: { // ZoneChange is received by the zone the player is in, then the @@ -1733,27 +1734,23 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { } break; } - case ServerOP_UsertoWorldCancelOfflineResponse: { - auto utwr = reinterpret_cast(pack->pBuffer); + case ServerOP_ReclaimOfflineSessionResp: { + if (pack->size != sizeof(OfflineSessionReclaim_Struct)) { + break; + } - ServerPacket server_packet; - server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; - server_packet.size = sizeof(UsertoWorldResponse); - server_packet.pBuffer = new uchar[server_packet.size]; - memset(server_packet.pBuffer, 0, server_packet.size); + auto reclaim = reinterpret_cast(pack->pBuffer); + auto client = ClientList::Instance()->FindByAccountID(reclaim->account_id); + if (!client) { + LogInfo( + "Ignoring offline reclaim response [{}] for account [{}]; world client not found", + reclaim->request_id, + reclaim->account_id + ); + break; + } - auto utwrs = reinterpret_cast(server_packet.pBuffer); - utwrs->lsaccountid = utwr->lsaccountid; - utwrs->ToID = utwr->FromID; - utwrs->worldid = utwr->worldid; - utwrs->response = UserToWorldStatusSuccess; - strn0cpy(utwrs->login, utwr->login, 64); - - LogLoginserverDetail( - "Step 7a - World received ServerOP_UsertoWorldCancelOfflineResponse back to login with success." - ); - - LoginServerList::Instance()->SendPacket(&server_packet); + client->HandleOfflineSessionReclaimResponse(*reclaim); break; } default: { diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index c81266957..d2e20ab92 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -24,9 +24,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "common/patches/patches.h" #include "common/profanity_manager.h" #include "common/repositories/account_repository.h" +#include "common/repositories/buyer_repository.h" #include "common/repositories/guild_tributes_repository.h" #include "common/repositories/character_offline_transactions_repository.h" #include "common/repositories/offline_character_sessions_repository.h" +#include "common/repositories/trader_repository.h" #include "common/rulesys.h" #include "common/say_link.h" #include "common/server_reload_types.h" @@ -57,9 +59,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include -#include "common/repositories/account_repository.h" -#include "common/repositories/character_offline_transactions_repository.h" - extern EntityList entity_list; extern Zone *zone; extern volatile bool is_zone_loaded; @@ -73,6 +72,46 @@ void Shutdown(); // QuestParserCollection *parse = 0; +namespace { +void SendOfflineSessionReclaimResponse(const OfflineSessionReclaim_Struct &request, int8 response) +{ + auto packet = new ServerPacket(ServerOP_ReclaimOfflineSessionResp, sizeof(OfflineSessionReclaim_Struct)); + auto out = reinterpret_cast(packet->pBuffer); + *out = request; + out->response = response; + worldserver.SendPacket(packet); + safe_delete(packet); +} + +bool HasActiveTraderTransaction(uint32 character_id) +{ + if (!character_id) { + return false; + } + + const auto active_entries = TraderRepository::GetWhere( + database, + fmt::format("`character_id` = {} AND `active_transaction` = 1 LIMIT 1", character_id) + ); + + return !active_entries.empty(); +} + +Client *FindOfflineReclaimClient(const OfflineSessionReclaim_Struct &request) +{ + Client *client = nullptr; + if (request.entity_id) { + client = entity_list.GetClientByID(request.entity_id); + } + + if (!client && request.character_id) { + client = entity_list.GetClientByCharID(request.character_id); + } + + return client; +} +} + WorldServer::WorldServer() { cur_groupid = 0; @@ -4361,122 +4400,116 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } break; } - case ServerOP_UsertoWorldCancelOfflineRequest: { - auto in = reinterpret_cast(pack->pBuffer); - auto client = entity_list.GetClientByLSID(in->lsaccountid); - if (!client) { - LogLoginserverDetail("Step 6a(1) - Zone received ServerOP_UsertoWorldCancelOfflineRequest though could " - "not find client." + case ServerOP_ReclaimOfflineSessionReq: { + if (pack->size != sizeof(OfflineSessionReclaim_Struct)) { + break; + } + + auto in = reinterpret_cast(pack->pBuffer); + auto client = FindOfflineReclaimClient(*in); + if (!client) { + LogInfo( + "Offline reclaim request [{}] for account [{}] character [{}] found no matching zone entity; reporting stale", + in->request_id, + in->account_id, + in->character_id ); + SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimStale); + break; + } - auto e = AccountRepository::GetWhere(database, fmt::format("`lsaccount_id` = '{}'", in->lsaccountid)); - if (!e.empty()) { - auto r = e.front(); - auto session = OfflineCharacterSessionsRepository::GetByAccountId(database, r.id); - auto trader = TraderRepository::GetAccountZoneIdAndInstanceIdByAccountId(database, r.id); - const uint32 character_id = session.id ? session.character_id : trader.character_id; + const bool account_matches = client->AccountID() == in->account_id; + const bool character_matches = client->CharacterID() == in->character_id; + const bool mode_matches = + (in->mode == OfflineSessionModeTrader && client->IsTrader()) || + (in->mode == OfflineSessionModeBuyer && client->IsBuyer()) || + (in->mode == OfflineSessionModeNone && (client->IsTrader() || client->IsBuyer())); - database.TransactionBegin(); - r.offline = 0; - AccountRepository::UpdateOne(database, r); - OfflineCharacterSessionsRepository::DeleteByAccountId(database, r.id); - if (character_id) { - TraderRepository::DeleteWhere(database, fmt::format("`character_id` = '{}'", character_id)); - BuyerRepository::DeleteBuyer(database, character_id); - } + if (in->mode == OfflineSessionModeNone) { + LogWarning( + "Offline reclaim request [{}] for account [{}] character [{}] had no mode; inferring active offline trade mode from zone entity [{}]", + in->request_id, + in->account_id, + in->character_id, + client->GetCleanName() + ); + } - auto commit_result = database.TransactionCommit(); - if (!commit_result.Success()) { - database.TransactionRollback(); - LogError( - "Failed clearing orphaned offline session state for account [{}]: ({}) {}", - r.id, - commit_result.ErrorNumber(), - commit_result.ErrorMessage() - ); - } + if (!client->IsOffline() || !account_matches || !character_matches || !mode_matches) { + LogWarning( + "Offline reclaim request [{}] matched client [{}] but state did not validate. offline [{}] account_match [{}] character_match [{}] mode_match [{}]", + in->request_id, + client->GetCleanName(), + client->IsOffline(), + account_matches, + character_matches, + mode_matches + ); + SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimFailed); + break; + } - LogLoginserverDetail( - "Step 6a(2) - Zone cleared offline status in account table for user id {} / {}", - r.lsaccount_id, - r.charname - ); - } + const bool has_customer = client->IsThereACustomer(); + const bool has_active_trader_transaction = client->IsTrader() && HasActiveTraderTransaction(client->CharacterID()); + if (has_customer || has_active_trader_transaction) { + LogInfo( + "Offline reclaim request [{}] for client [{}] is busy; customer [{}] trader_transaction [{}]", + in->request_id, + client->GetCleanName(), + has_customer, + has_active_trader_transaction + ); + SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimBusy); + break; + } - - auto sp = new ServerPacket(ServerOP_UsertoWorldCancelOfflineResponse, pack->size); - auto out = reinterpret_cast(sp->pBuffer); - sp->opcode = ServerOP_UsertoWorldCancelOfflineResponse; - out->FromID = in->FromID; - out->lsaccountid = in->lsaccountid; - out->response = in->response; - out->ToID = in->ToID; - out->worldid = in->worldid; - strn0cpy(out->login, in->login, 64); - - LogLoginserverDetail("Step 6a(3) - Zone sending ServerOP_UsertoWorldCancelOfflineResponse back to world"); - worldserver.SendPacket(sp); - safe_delete(sp); - break; - } - - LogLoginserverDetail( - "Step 6b(1) - Zone received ServerOP_UsertoWorldCancelOfflineRequest and found client {}", - client->GetCleanName() - ); - LogLoginserverDetail( - "Step 6b(2) - Zone cleared offline status in account table for user id {} / {}", - client->CharacterID(), - client->GetCleanName() + LogInfo( + "Reclaiming offline {} [{}] for account [{}] character [{}]", + client->IsBuyer() ? "buyer" : "trader", + client->GetCleanName(), + client->AccountID(), + client->CharacterID() ); + + database.TransactionBegin(); AccountRepository::SetOfflineStatus(database, client->AccountID(), false); OfflineCharacterSessionsRepository::DeleteByAccountId(database, client->AccountID()); + auto commit_result = database.TransactionCommit(); + if (!commit_result.Success()) { + database.TransactionRollback(); + LogError( + "Failed clearing offline session state for account [{}] character [{}] during reclaim request [{}]: ({}) {}", + client->AccountID(), + client->CharacterID(), + in->request_id, + commit_result.ErrorNumber(), + commit_result.ErrorMessage() + ); + SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimFailed); + break; + } - if (client->IsThereACustomer()) { - auto customer = entity_list.GetClientByID(client->GetCustomerID()); - if (customer) { - auto end_session = new EQApplicationPacket(OP_ShopEnd); - customer->FastQueuePacket(&end_session); - } - } + if (client->IsTrader()) { + client->TraderEndTrader(); + } - if (client->IsTrader()) { - LogLoginserverDetail("Step 6b(3) - Zone ending trader mode for client {}", client->GetCleanName()); - client->TraderEndTrader(); - } + if (client->IsBuyer()) { + client->ToggleBuyerMode(false); + } - if (client->IsBuyer()) { - LogLoginserverDetail("Step 6b(4) - Zone ending buyer mode for client {}", client->GetCleanName()); - client->ToggleBuyerMode(false); - } - - LogLoginserverDetail("Step 6b(5) - Zone updating UpdateWho(2) for client {}", client->GetCleanName()); client->UpdateWho(2); - auto outapp = new EQApplicationPacket(); - LogLoginserverDetail("Step 6b(6) - Zone sending despawn packet for client {}", client->GetCleanName()); - client->CreateDespawnPacket(outapp, false); - entity_list.QueueClients(nullptr, outapp, false); - safe_delete(outapp); + auto outapp = new EQApplicationPacket(); + client->CreateDespawnPacket(outapp, false); + entity_list.QueueClients(nullptr, outapp, false); + safe_delete(outapp); - LogLoginserverDetail("Step 6b(7) - Zone removing client from entity_list"); - entity_list.RemoveMob(client->CastToMob()->GetID()); + auto delete_id = client->CastToMob()->GetID(); + entity_list.RemoveMob(delete_id); - auto sp = new ServerPacket(ServerOP_UsertoWorldCancelOfflineResponse, pack->size); - auto out = reinterpret_cast(sp->pBuffer); - sp->opcode = ServerOP_UsertoWorldCancelOfflineResponse; - out->FromID = in->FromID; - out->lsaccountid = in->lsaccountid; - out->response = in->response; - out->ToID = in->ToID; - out->worldid = in->worldid; - strn0cpy(out->login, in->login, 64); - - LogLoginserverDetail("Step 6b(8) - Zone sending ServerOP_UsertoWorldCancelOfflineResponse back to world"); - worldserver.SendPacket(sp); - safe_delete(sp); - break; - } + SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimSuccess); + break; + } default: { LogInfo("Unknown ZS Opcode [{}] size [{}]", (int) pack->opcode, pack->size); break;