mirror of
https://github.com/EQEmu/Server.git
synced 2026-04-19 16:52:25 +00:00
Pivot offline reclaim to world-owned flow
This commit is contained in:
parent
24c15b16fe
commit
fa13039bf7
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -11,5 +11,3 @@ OP_Poll=0x0029
|
||||
OP_LoginExpansionPacketData=0x0031
|
||||
OP_EnterChat=0x000f
|
||||
OP_PollResponse=0x0011
|
||||
OP_CancelOfflineTrader=0x0016
|
||||
OP_CancelOfflineTraderResponse=0x0030
|
||||
|
||||
@ -52,12 +52,6 @@ WorldServer::WorldServer(std::shared_ptr<EQ::Net::ServertalkServerConnection> 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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<EQ::Net::ServertalkServerConnection> m_connection;
|
||||
|
||||
|
||||
@ -216,35 +216,3 @@ const std::list<std::unique_ptr<WorldServer>> &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<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);
|
||||
}
|
||||
}
|
||||
@ -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<EQApplicationPacket> 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);
|
||||
|
||||
283
world/client.cpp
283
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 <climits>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@ -88,6 +92,60 @@ extern uint32 numclients;
|
||||
extern volatile bool RunLoops;
|
||||
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
|
||||
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<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;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<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);
|
||||
}
|
||||
|
||||
@ -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<EQ::Timer> m_keepalive;
|
||||
|
||||
|
||||
@ -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<UsertoWorldResponse *>(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<OfflineSessionReclaim_Struct *>(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<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);
|
||||
|
||||
LogLoginserverDetail(
|
||||
"Step 7a - World received ServerOP_UsertoWorldCancelOfflineResponse back to login with success."
|
||||
);
|
||||
|
||||
LoginServerList::Instance()->SendPacket(&server_packet);
|
||||
client->HandleOfflineSessionReclaimResponse(*reclaim);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@ -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 <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#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<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()
|
||||
{
|
||||
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<UsertoWorldResponse *>(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<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;
|
||||
}
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
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<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;
|
||||
}
|
||||
SendOfflineSessionReclaimResponse(*in, OfflineSessionReclaimSuccess);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LogInfo("Unknown ZS Opcode [{}] size [{}]", (int) pack->opcode, pack->size);
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user