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
+283
View File
@@ -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) {
+14
View File
@@ -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();
-126
View File
@@ -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);
}
-1
View File
@@ -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;
+16 -19
View File
@@ -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: {