Pivot offline reclaim to world-owned flow

This commit is contained in:
Vayle 2026-03-20 22:07:50 -04:00
parent 24c15b16fe
commit fa13039bf7
17 changed files with 497 additions and 471 deletions

View File

@ -71,8 +71,6 @@ N(OP_BuyerItems),
N(OP_CameraEffect), N(OP_CameraEffect),
N(OP_Camp), N(OP_Camp),
N(OP_CancelSneakHide), N(OP_CancelSneakHide),
N(OP_CancelOfflineTrader),
N(OP_CancelOfflineTraderResponse),
N(OP_CancelTask), N(OP_CancelTask),
N(OP_CancelTrade), N(OP_CancelTrade),
N(OP_CashReward), N(OP_CashReward),

View File

@ -233,8 +233,8 @@
#define ServerOP_UsertoWorldRespLeg 0xAB01 #define ServerOP_UsertoWorldRespLeg 0xAB01
#define ServerOP_UsertoWorldReq 0xAB02 #define ServerOP_UsertoWorldReq 0xAB02
#define ServerOP_UsertoWorldResp 0xAB03 #define ServerOP_UsertoWorldResp 0xAB03
#define ServerOP_UsertoWorldCancelOfflineRequest 0xAB04 #define ServerOP_ReclaimOfflineSessionReq 0xAB04
#define ServerOP_UsertoWorldCancelOfflineResponse 0xAB05 #define ServerOP_ReclaimOfflineSessionResp 0xAB05
#define ServerOP_LauncherConnectInfo 0x3000 #define ServerOP_LauncherConnectInfo 0x3000
#define ServerOP_LauncherZoneRequest 0x3001 #define ServerOP_LauncherZoneRequest 0x3001
@ -367,8 +367,7 @@ enum {
UserToWorldStatusSuspended = -1, UserToWorldStatusSuspended = -1,
UserToWorldStatusBanned = -2, UserToWorldStatusBanned = -2,
UserToWorldStatusWorldAtCapacity = -3, UserToWorldStatusWorldAtCapacity = -3,
UserToWorldStatusAlreadyOnline = -4, UserToWorldStatusAlreadyOnline = -4
UserToWorldStatusOffilineTraderBuyer = -5
}; };
enum { enum {
@ -380,6 +379,19 @@ enum {
BazaarPurchaseBuyerSuccess = 5, BazaarPurchaseBuyerSuccess = 5,
BazaarPurchaseTraderFailed = 6 BazaarPurchaseTraderFailed = 6
}; };
enum : uint8 {
OfflineSessionModeNone = 0,
OfflineSessionModeTrader = 1,
OfflineSessionModeBuyer = 2
};
enum : int8 {
OfflineSessionReclaimFailed = 0,
OfflineSessionReclaimSuccess = 1,
OfflineSessionReclaimStale = 2,
OfflineSessionReclaimBusy = 3
};
/************ PACKET RELATED STRUCT ************/ /************ PACKET RELATED STRUCT ************/
class ServerPacket class ServerPacket
{ {
@ -847,6 +859,18 @@ struct WorldToZone_Struct {
uint32 account_id; uint32 account_id;
int8 response; 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 { struct WorldShutDown_Struct {
uint32 time; uint32 time;
uint32 interval; uint32 interval;

View File

@ -1,9 +1,11 @@
# Bazaar Item Unique ID And Offline Trading Rollout # Bazaar Item Unique ID And World-Only Offline Trading Rollout
## Purpose ## 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. 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. Do not reopen the server until every verification step passes.
## Preconditions ## Preconditions
@ -11,6 +13,8 @@ Do not reopen the server until every verification step passes.
- Schedule a maintenance window. - Schedule a maintenance window.
- Stop new logins before beginning the migration. - Stop new logins before beginning the migration.
- Ensure the `world` binary you are deploying includes this branch. - 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. - Ensure operators have credentials to run schema updates and database dump commands.
## Mandatory Backup ## Mandatory Backup
@ -40,6 +44,9 @@ Validate these gameplay scenarios after the migration:
- One trader changing a price without affecting another trader. - One trader changing a price without affecting another trader.
- Offline trader purchase. - Offline trader purchase.
- Offline buyer 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. - Parcel retrieval for rows that previously had missing `item_unique_id` values.
- Alternate bazaar shard search. - Alternate bazaar shard search.
@ -76,6 +83,13 @@ world database:item-unique-ids --verify --verbose
8. Reopen the server only after verification passes. 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 ## What Preflight And Verify Must Show
The migration is not complete unless all of the following are true: The migration is not complete unless all of the following are true:

View File

@ -71,25 +71,6 @@ bool Client::Process()
SendPlayToWorld((const char *) app->pBuffer); SendPlayToWorld((const char *) app->pBuffer);
break; 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; delete app;
@ -580,27 +561,3 @@ std::string Client::GetClientLoggingDescription()
client_ip 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);
}

View File

@ -25,7 +25,6 @@ public:
void SendPlayToWorld(const char *data); void SendPlayToWorld(const char *data);
void SendServerListPacket(uint32 seq); void SendServerListPacket(uint32 seq);
void SendPlayResponse(EQApplicationPacket *outapp); void SendPlayResponse(EQApplicationPacket *outapp);
void SendCancelOfflineStatusToWorld(const char *data);
void GenerateRandomLoginKey(); void GenerateRandomLoginKey();
unsigned int GetAccountID() const { return m_account_id; } unsigned int GetAccountID() const { return m_account_id; }
std::string GetLoginServerName() const { return m_loginserver_name; } std::string GetLoginServerName() const { return m_loginserver_name; }

View File

@ -83,11 +83,6 @@ struct PlayEverquestResponse {
uint32 server_number; uint32 server_number;
}; };
struct CancelOfflineTrader {
LoginBaseMessage base_header;
int16_t unk;
};
#pragma pack() #pragma pack()
enum LSClientVersion { enum LSClientVersion {
@ -163,7 +158,6 @@ namespace LS {
constexpr static int ERROR_NONE = 101; // No Error constexpr static int ERROR_NONE = 101; // No Error
constexpr static int ERROR_UNKNOWN = 102; // Error - Unknown Error Occurred 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_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_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_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_ACCOUNT_BANNED = 338; // This account is currently banned. Please contact customer service for more information.

View File

@ -11,5 +11,3 @@ OP_Poll=0x0029
OP_LoginExpansionPacketData=0x0031 OP_LoginExpansionPacketData=0x0031
OP_EnterChat=0x000f OP_EnterChat=0x000f
OP_PollResponse=0x0011 OP_PollResponse=0x0011
OP_CancelOfflineTrader=0x0016
OP_CancelOfflineTraderResponse=0x0030

View File

@ -52,12 +52,6 @@ WorldServer::WorldServer(std::shared_ptr<EQ::Net::ServertalkServerConnection> wo
ServerOP_LSAccountUpdate, ServerOP_LSAccountUpdate,
std::bind(&WorldServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2) 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; WorldServer::~WorldServer() = default;
@ -305,10 +299,6 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
case UserToWorldStatusAlreadyOnline: case UserToWorldStatusAlreadyOnline:
r->base_reply.error_str_id = LS::ErrStr::ERROR_ACTIVE_CHARACTER; r->base_reply.error_str_id = LS::ErrStr::ERROR_ACTIVE_CHARACTER;
break; break;
case UserToWorldStatusOffilineTraderBuyer:
r->base_reply.success = false;
r->base_reply.error_str_id = LS::ErrStr::ERROR_OFFLINE_TRADER;
break;
default: default:
r->base_reply.error_str_id = LS::ErrStr::ERROR_UNKNOWN; r->base_reply.error_str_id = LS::ErrStr::ERROR_UNKNOWN;
break; break;
@ -785,113 +775,3 @@ void WorldServer::FormatWorldServerName(char *name, int8 server_list_type)
strn0cpy(name, server_long_name.c_str(), 201); 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<PlayEverquestResponse *>(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<PlayEverquestResponse *>(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
);
}
}

View File

@ -58,7 +58,6 @@ private:
void ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Net::Packet &packet); void ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Net::Packet &packet);
void ProcessUserToWorldResponse(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 ProcessLSAccountUpdate(uint16_t opcode, const EQ::Net::Packet &packet);
void ProcessUserToWorldCancelOfflineResponse(uint16_t opcode, const EQ::Net::Packet &packet);
std::shared_ptr<EQ::Net::ServertalkServerConnection> m_connection; std::shared_ptr<EQ::Net::ServertalkServerConnection> m_connection;

View File

@ -216,35 +216,3 @@ const std::list<std::unique_ptr<WorldServer>> &WorldServerManager::GetWorldServe
{ {
return m_world_servers; 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<WorldServer> &server) {
return server->GetServerId() == server_id;
}
);
if (iter != m_world_servers.end()) {
EQ::Net::DynamicPacket outapp;
outapp.Resize(sizeof(UsertoWorldRequest));
auto *r = reinterpret_cast<UsertoWorldRequest *>(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);
}
}

View File

@ -16,11 +16,6 @@ public:
unsigned int client_account_id, unsigned int client_account_id,
const std::string &client_loginserver const std::string &client_loginserver
); );
void SendUserToWorldCancelOfflineRequest(
unsigned int server_id,
unsigned int client_account_id,
const std::string &client_loginserver
);
std::unique_ptr<EQApplicationPacket> CreateServerListPacket(Client *client, uint32 sequence); std::unique_ptr<EQApplicationPacket> CreateServerListPacket(Client *client, uint32 sequence);
bool DoesServerExist(const std::string &s, const std::string &server_short_name, WorldServer *ignore = nullptr); 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); void DestroyServerByName(std::string s, std::string server_short_name, WorldServer *ignore = nullptr);

View File

@ -37,10 +37,13 @@
#include "common/races.h" #include "common/races.h"
#include "common/random.h" #include "common/random.h"
#include "common/repositories/account_repository.h" #include "common/repositories/account_repository.h"
#include "common/repositories/buyer_repository.h"
#include "common/repositories/character_data_repository.h" #include "common/repositories/character_data_repository.h"
#include "common/repositories/group_id_repository.h" #include "common/repositories/group_id_repository.h"
#include "common/repositories/inventory_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/player_event_logs_repository.h"
#include "common/repositories/trader_repository.h"
#include "common/rulesys.h" #include "common/rulesys.h"
#include "common/shareddb.h" #include "common/shareddb.h"
#include "common/skill_caps.h" #include "common/skill_caps.h"
@ -59,6 +62,7 @@
#include "zlib.h" #include "zlib.h"
#include <climits> #include <climits>
#include <atomic>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@ -88,6 +92,60 @@ extern uint32 numclients;
extern volatile bool RunLoops; extern volatile bool RunLoops;
extern volatile bool UCSServerAvailable_; extern volatile bool UCSServerAvailable_;
namespace {
constexpr uint32 kOfflineSessionReclaimTimeoutMs = 10000;
std::atomic<uint32> 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 // unused ATM, but here for reference, should match RoF2
enum class NameApprovalResponse : int { enum class NameApprovalResponse : int {
NotValid = -1, // string ID 1576 NotValid = -1, // string ID 1576
@ -102,6 +160,7 @@ enum class NameApprovalResponse : int {
Client::Client(EQStreamInterface* ieqs) Client::Client(EQStreamInterface* ieqs)
: autobootup_timeout(RuleI(World, ZoneAutobootTimeoutMS)), : autobootup_timeout(RuleI(World, ZoneAutobootTimeoutMS)),
connect(1000), connect(1000),
offline_reclaim_timeout(kOfflineSessionReclaimTimeoutMs),
eqs(ieqs) eqs(ieqs)
{ {
// Live does not send datarate as of 3/11/2005 // Live does not send datarate as of 3/11/2005
@ -111,6 +170,7 @@ Client::Client(EQStreamInterface* ieqs)
autobootup_timeout.Disable(); autobootup_timeout.Disable();
connect.Disable(); connect.Disable();
offline_reclaim_timeout.Disable();
seen_character_select = false; seen_character_select = false;
cle = 0; cle = 0;
zone_id = 0; zone_id = 0;
@ -119,6 +179,14 @@ Client::Client(EQStreamInterface* ieqs)
zone_waiting_for_bootup = 0; zone_waiting_for_bootup = 0;
enter_world_triggered = false; enter_world_triggered = false;
StartInTutorial = 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_ClientVersion = eqs->ClientVersion();
m_ClientVersionBit = EQ::versions::ConvertClientVersionToClientVersionBit(m_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) { if(!is_player_zoning) {
GroupIdRepository::DeleteWhere( GroupIdRepository::DeleteWhere(
database, database,
@ -1066,10 +1145,201 @@ bool Client::HandleEnterWorldPacket(const EQApplicationPacket *app) {
} }
EnterWorld(); 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<OfflineSessionReclaim_Struct *>(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; 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) { bool Client::HandleDeleteCharacterPacket(const EQApplicationPacket *app) {
uint32 char_acct_id = database.GetAccountIDByChar((char*)app->pBuffer); uint32 char_acct_id = database.GetAccountIDByChar((char*)app->pBuffer);
@ -1223,6 +1493,18 @@ bool Client::Process() {
TellClientZoneUnavailable(); 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()){ if(connect.Check()){
SendGuildList();// Send OPCode: OP_GuildsList SendGuildList();// Send OPCode: OP_GuildsList
SendApproveWorld(); SendApproveWorld();
@ -1601,6 +1883,7 @@ void Client::TellClientZoneUnavailable() {
zone_waiting_for_bootup = 0; zone_waiting_for_bootup = 0;
enter_world_triggered = false; enter_world_triggered = false;
autobootup_timeout.Disable(); autobootup_timeout.Disable();
ResetOfflineSessionReclaimState();
} }
void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req) { void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req) {

View File

@ -51,6 +51,7 @@ public:
void SendPostEnterWorld(); void SendPostEnterWorld();
void SendGuildTributeFavorAndTimer(uint32 favor, uint32 time_remaining); void SendGuildTributeFavorAndTimer(uint32 favor, uint32 time_remaining);
void SendGuildTributeOptInToggle(const GuildTributeMemberToggle* in); void SendGuildTributeOptInToggle(const GuildTributeMemberToggle* in);
void HandleOfflineSessionReclaimResponse(const OfflineSessionReclaim_Struct &response);
inline uint32 GetIP() { return ip; } inline uint32 GetIP() { return ip; }
inline uint16 GetPort() { return port; } inline uint16 GetPort() { return port; }
@ -100,6 +101,15 @@ private:
ClientListEntry* cle; ClientListEntry* cle;
Timer connect; Timer connect;
bool seen_character_select; 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 HandlePacket(const EQApplicationPacket *app);
bool HandleNameApprovalPacket(const EQApplicationPacket *app); bool HandleNameApprovalPacket(const EQApplicationPacket *app);
@ -114,6 +124,10 @@ private:
bool ChecksumVerificationCRCEQGame(uint64 checksum); bool ChecksumVerificationCRCEQGame(uint64 checksum);
bool ChecksumVerificationCRCSkillCaps(uint64 checksum); bool ChecksumVerificationCRCSkillCaps(uint64 checksum);
bool ChecksumVerificationCRCBaseData(uint64 checksum); bool ChecksumVerificationCRCBaseData(uint64 checksum);
void ContinueEnterWorld();
bool BeginOfflineSessionReclaimIfNeeded();
bool ClearStaleOfflineSession(uint32 character_id, const char *reason);
void ResetOfflineSessionReclaimState();
EQStreamInterface* eqs; EQStreamInterface* eqs;
bool CanTradeFVNoDropItem(); bool CanTradeFVNoDropItem();

View File

@ -4,11 +4,6 @@
#include "common/eqemu_logsys.h" #include "common/eqemu_logsys.h"
#include "common/misc_functions.h" #include "common/misc_functions.h"
#include "common/packet_dump.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/servertalk.h"
#include "common/strings.h" #include "common/strings.h"
#include "common/version.h" #include "common/version.h"
@ -195,13 +190,6 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet &p)
return; 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 (RuleB(World, EnforceCharacterLimitAtLogin)) {
if (ClientList::Instance()->IsAccountInGame(utwr->lsaccountid)) { if (ClientList::Instance()->IsAccountInGame(utwr->lsaccountid)) {
LogDebug("User already online account_id [{0}]", utwr->lsaccountid); LogDebug("User already online account_id [{0}]", utwr->lsaccountid);
@ -589,14 +577,6 @@ bool LoginServer::Connect()
std::placeholders::_2 std::placeholders::_2
) )
); );
m_client->OnMessage(
ServerOP_UsertoWorldCancelOfflineRequest,
std::bind(
&LoginServer::ProcessUserToWorldCancelOfflineRequest,
this,
std::placeholders::_1,
std::placeholders::_2)
);
} }
return true; return true;
@ -711,109 +691,3 @@ void LoginServer::SendAccountUpdate(ServerPacket *pack)
SendPacket(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<UsertoWorldRequest *>(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<UsertoWorldResponse *>(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<int32>(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);
}

View File

@ -49,7 +49,6 @@ private:
void ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet &p); void ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet &p);
void ProcessLSRemoteAddr(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 ProcessLSAccountUpdate(uint16_t opcode, EQ::Net::Packet &p);
void ProcessUserToWorldCancelOfflineRequest(uint16_t opcode, EQ::Net::Packet &p);
std::unique_ptr<EQ::Timer> m_keepalive; std::unique_ptr<EQ::Timer> m_keepalive;

View File

@ -779,6 +779,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
if (client) { if (client) {
client->Clearance(wtz->response); client->Clearance(wtz->response);
} }
break;
} }
case ServerOP_ZoneToZoneRequest: { case ServerOP_ZoneToZoneRequest: {
// ZoneChange is received by the zone the player is in, then the // 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; break;
} }
case ServerOP_UsertoWorldCancelOfflineResponse: { case ServerOP_ReclaimOfflineSessionResp: {
auto utwr = reinterpret_cast<UsertoWorldResponse *>(pack->pBuffer); if (pack->size != sizeof(OfflineSessionReclaim_Struct)) {
break;
}
ServerPacket server_packet; auto reclaim = reinterpret_cast<OfflineSessionReclaim_Struct *>(pack->pBuffer);
server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; auto client = ClientList::Instance()->FindByAccountID(reclaim->account_id);
server_packet.size = sizeof(UsertoWorldResponse); if (!client) {
server_packet.pBuffer = new uchar[server_packet.size]; LogInfo(
memset(server_packet.pBuffer, 0, server_packet.size); "Ignoring offline reclaim response [{}] for account [{}]; world client not found",
reclaim->request_id,
auto utwrs = reinterpret_cast<UsertoWorldResponse *>(server_packet.pBuffer); reclaim->account_id
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."
); );
break;
}
LoginServerList::Instance()->SendPacket(&server_packet); client->HandleOfflineSessionReclaimResponse(*reclaim);
break; break;
} }
default: { default: {

View File

@ -24,9 +24,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include "common/patches/patches.h" #include "common/patches/patches.h"
#include "common/profanity_manager.h" #include "common/profanity_manager.h"
#include "common/repositories/account_repository.h" #include "common/repositories/account_repository.h"
#include "common/repositories/buyer_repository.h"
#include "common/repositories/guild_tributes_repository.h" #include "common/repositories/guild_tributes_repository.h"
#include "common/repositories/character_offline_transactions_repository.h" #include "common/repositories/character_offline_transactions_repository.h"
#include "common/repositories/offline_character_sessions_repository.h" #include "common/repositories/offline_character_sessions_repository.h"
#include "common/repositories/trader_repository.h"
#include "common/rulesys.h" #include "common/rulesys.h"
#include "common/say_link.h" #include "common/say_link.h"
#include "common/server_reload_types.h" #include "common/server_reload_types.h"
@ -57,9 +59,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include "common/repositories/account_repository.h"
#include "common/repositories/character_offline_transactions_repository.h"
extern EntityList entity_list; extern EntityList entity_list;
extern Zone *zone; extern Zone *zone;
extern volatile bool is_zone_loaded; extern volatile bool is_zone_loaded;
@ -73,6 +72,46 @@ void Shutdown();
// QuestParserCollection *parse = 0; // 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<OfflineSessionReclaim_Struct *>(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() WorldServer::WorldServer()
{ {
cur_groupid = 0; cur_groupid = 0;
@ -4361,120 +4400,114 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
} }
break; break;
} }
case ServerOP_UsertoWorldCancelOfflineRequest: { case ServerOP_ReclaimOfflineSessionReq: {
auto in = reinterpret_cast<UsertoWorldResponse *>(pack->pBuffer); if (pack->size != sizeof(OfflineSessionReclaim_Struct)) {
auto client = entity_list.GetClientByLSID(in->lsaccountid); break;
if (!client) {
LogLoginserverDetail("Step 6a(1) - Zone received ServerOP_UsertoWorldCancelOfflineRequest though could "
"not find client."
);
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;
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);
} }
auto in = reinterpret_cast<OfflineSessionReclaim_Struct *>(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;
}
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()));
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()
);
}
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;
}
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;
}
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(); auto commit_result = database.TransactionCommit();
if (!commit_result.Success()) { if (!commit_result.Success()) {
database.TransactionRollback(); database.TransactionRollback();
LogError( LogError(
"Failed clearing orphaned offline session state for account [{}]: ({}) {}", "Failed clearing offline session state for account [{}] character [{}] during reclaim request [{}]: ({}) {}",
r.id, client->AccountID(),
client->CharacterID(),
in->request_id,
commit_result.ErrorNumber(), commit_result.ErrorNumber(),
commit_result.ErrorMessage() commit_result.ErrorMessage()
); );
} SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimFailed);
LogLoginserverDetail(
"Step 6a(2) - Zone cleared offline status in account table for user id {} / {}",
r.lsaccount_id,
r.charname
);
}
auto sp = new ServerPacket(ServerOP_UsertoWorldCancelOfflineResponse, pack->size);
auto out = reinterpret_cast<UsertoWorldResponse *>(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; 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()
);
AccountRepository::SetOfflineStatus(database, client->AccountID(), false);
OfflineCharacterSessionsRepository::DeleteByAccountId(database, client->AccountID());
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()) { if (client->IsTrader()) {
LogLoginserverDetail("Step 6b(3) - Zone ending trader mode for client {}", client->GetCleanName());
client->TraderEndTrader(); client->TraderEndTrader();
} }
if (client->IsBuyer()) { if (client->IsBuyer()) {
LogLoginserverDetail("Step 6b(4) - Zone ending buyer mode for client {}", client->GetCleanName());
client->ToggleBuyerMode(false); client->ToggleBuyerMode(false);
} }
LogLoginserverDetail("Step 6b(5) - Zone updating UpdateWho(2) for client {}", client->GetCleanName());
client->UpdateWho(2); client->UpdateWho(2);
auto outapp = new EQApplicationPacket(); auto outapp = new EQApplicationPacket();
LogLoginserverDetail("Step 6b(6) - Zone sending despawn packet for client {}", client->GetCleanName());
client->CreateDespawnPacket(outapp, false); client->CreateDespawnPacket(outapp, false);
entity_list.QueueClients(nullptr, outapp, false); entity_list.QueueClients(nullptr, outapp, false);
safe_delete(outapp); safe_delete(outapp);
LogLoginserverDetail("Step 6b(7) - Zone removing client from entity_list"); auto delete_id = client->CastToMob()->GetID();
entity_list.RemoveMob(client->CastToMob()->GetID()); entity_list.RemoveMob(delete_id);
auto sp = new ServerPacket(ServerOP_UsertoWorldCancelOfflineResponse, pack->size); SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimSuccess);
auto out = reinterpret_cast<UsertoWorldResponse *>(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; break;
} }
default: { default: {