diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 1a9028eb7..40d06e735 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -186,6 +186,7 @@ SET(repositories repositories/base/base_character_leadership_abilities_repository.h repositories/base/base_character_material_repository.h repositories/base/base_character_memmed_spells_repository.h + repositories/base/base_character_offline_transactions_repository.h repositories/base/base_character_parcels_repository.h repositories/base/base_character_parcels_containers_repository.h repositories/base/base_character_peqzone_flags_repository.h @@ -382,6 +383,7 @@ SET(repositories repositories/character_leadership_abilities_repository.h repositories/character_material_repository.h repositories/character_memmed_spells_repository.h + repositories/character_offline_transactions_repository.h repositories/character_parcels_repository.h repositories/character_parcels_containers_repository.h repositories/character_peqzone_flags_repository.h diff --git a/common/database.cpp b/common/database.cpp index 51c0c6429..d60f3ce60 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -210,7 +210,7 @@ void Database::LoginIP(uint32 account_id, const std::string& login_ip) QueryDatabase(query); } -int16 Database::GetAccountStatus(uint32 account_id) +AccountStatus::StatusRecord Database::GetAccountStatus(uint32 account_id) { auto e = AccountRepository::FindOne(*this, account_id); @@ -222,7 +222,11 @@ int16 Database::GetAccountStatus(uint32 account_id) AccountRepository::UpdateOne(*this, e); } - return e.status; + AccountStatus::StatusRecord result{}; + result.status = e.status; + result.offline = e.offline; + + return result; } uint32 Database::CreateAccount( @@ -2265,6 +2269,7 @@ void Database::ClearGuildOnlineStatus() void Database::ClearTraderDetails() { TraderRepository::Truncate(*this); + AccountRepository::ClearAllOfflineStatus(*this); } void Database::ClearBuyerDetails() diff --git a/common/database.h b/common/database.h index 3a372eec9..accbb55ff 100644 --- a/common/database.h +++ b/common/database.h @@ -172,8 +172,8 @@ public: const std::string GetLiveChar(uint32 account_id); bool SetAccountStatus(const std::string& account_name, int16 status); bool SetLocalPassword(uint32 account_id, const std::string& password); + AccountStatus::StatusRecord GetAccountStatus(uint32 account_id); bool UpdateLiveChar(const std::string& name, uint32 account_id); - int16 GetAccountStatus(uint32 account_id); void SetAccountCRCField(uint32 account_id, const std::string& field_name, uint64 checksum); uint32 CheckLogin(const std::string& name, const std::string& password, const std::string& loginserver, int16* status = 0); uint32 CreateAccount( diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 98e8128b6..01c8d5228 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -7240,6 +7240,32 @@ ALTER TABLE `trader` )", .content_schema_update = false }, + ManifestEntry{ + .version = 9325, + .description = "2025_01_27_offline_account_status.sql", + .check = "SHOW COLUMNS FROM `account` LIKE 'offline'", + .condition = "empty", + .match = "", + .sql = R"( + ALTER TABLE `account` + ADD COLUMN `offline` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `time_creation`; + + CREATE TABLE `character_offline_transactions` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `character_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `type` INT(10) UNSIGNED NULL DEFAULT '0', + `item_name` VARCHAR(64) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci', + `quantity` INT(11) NULL DEFAULT '0', + `price` BIGINT(20) UNSIGNED NULL DEFAULT '0', + `buyer_name` VARCHAR(64) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_character_id` (`character_id`) + ) + COLLATE='latin1_swedish_ci' + ENGINE=InnoDB; + )", + .content_schema_update = false + }, // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/database_schema.h b/common/database_schema.h index de6f8472f..d606a96b1 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -59,6 +59,7 @@ namespace DatabaseSchema { {"character_leadership_abilities", "id"}, {"character_material", "id"}, {"character_memmed_spells", "id"}, + {"character_offline_transactions", "character_id"}, {"character_parcels", "char_id"}, {"character_parcels_containers", "id"}, {"character_pet_buffs", "char_id"}, @@ -134,6 +135,7 @@ namespace DatabaseSchema { "character_leadership_abilities", "character_material", "character_memmed_spells", + "character_offline_transactions", "character_parcels", "character_parcels_containers", "character_pet_buffs", diff --git a/common/emu_constants.h b/common/emu_constants.h index 0c762ffb8..c95a8f62e 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -46,6 +46,11 @@ namespace AccountStatus { constexpr uint8 Max = 255; std::string GetName(uint8 account_status); + + struct StatusRecord { + int16 status; + uint32 offline; + }; } static std::map account_status_names = { diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 268fa97cd..7e753a307 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -71,6 +71,8 @@ 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), @@ -381,6 +383,7 @@ N(OP_MultiLineMsg), N(OP_NewSpawn), N(OP_NewTitlesAvailable), N(OP_NewZone), +N(OP_Offline), N(OP_OnLevelMessage), N(OP_OpenContainer), N(OP_OpenDiscordMerchant), diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 71495ac37..8f2c1d08d 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -326,6 +326,7 @@ union bool buyer; bool untargetable; uint32 npc_tint_id; + bool offline; }; struct PlayerState_Struct { @@ -3913,12 +3914,12 @@ struct SimpleMessage_Struct{ }; struct GuildMemberUpdate_Struct { -/*00*/ uint32 GuildID; -/*04*/ char MemberName[64]; -/*68*/ uint16 ZoneID; -/*70*/ uint16 InstanceID; //speculated -/*72*/ uint32 LastSeen; //unix timestamp -/*76*/ + /*00*/ uint32 GuildID; + /*04*/ char MemberName[64]; + /*68*/ uint16 ZoneID; + /*72*/ uint16 InstanceID; //speculated + /*76*/ uint32 LastSeen; //unix timestamp + /*80*/ uint32 offline_mode; }; struct GuildMemberLevelUpdate_Struct { @@ -3941,6 +3942,7 @@ struct Internal_GuildMemberEntry_Struct { uint16 zoneinstance; //network byte order uint16 zone_id; //network byte order uint32 online; + uint32 offline_mode; }; struct Internal_GuildMembers_Struct { //just for display purposes, this is not actually used in the message encoding. diff --git a/common/events/player_events.h b/common/events/player_events.h index 4a3850cac..2ca03c0cc 100644 --- a/common/events/player_events.h +++ b/common/events/player_events.h @@ -1098,6 +1098,7 @@ namespace PlayerEvent { int32 charges; uint64 total_cost; uint64 player_money_balance; + bool offline_purchase; // cereal template @@ -1153,7 +1154,8 @@ namespace PlayerEvent { CEREAL_NVP(quantity), CEREAL_NVP(charges), CEREAL_NVP(total_cost), - CEREAL_NVP(player_money_balance) + CEREAL_NVP(player_money_balance), + CEREAL_NVP(offline_purchase) ); } }; @@ -1174,7 +1176,9 @@ namespace PlayerEvent { int32 charges; uint64 total_cost; uint64 player_money_balance; + bool offline_purchase; + // cereal template void serialize(Archive& ar) { @@ -1228,7 +1232,8 @@ namespace PlayerEvent { CEREAL_NVP(quantity), CEREAL_NVP(charges), CEREAL_NVP(total_cost), - CEREAL_NVP(player_money_balance) + CEREAL_NVP(player_money_balance), + CEREAL_NVP(offline_purchase) ); } }; diff --git a/common/guild_base.cpp b/common/guild_base.cpp index 3f34c3625..1e8be7228 100644 --- a/common/guild_base.cpp +++ b/common/guild_base.cpp @@ -844,8 +844,10 @@ bool BaseGuildManager::QueryWithLogging(std::string query, const char *errmsg) #define GuildMemberBaseQuery \ "SELECT c.`id`, c.`name`, c.`class`, c.`level`, c.`last_login`, c.`zone_id`," \ " g.`guild_id`, g.`rank`, g.`tribute_enable`, g.`total_tribute`, g.`last_tribute`," \ -" g.`banker`, g.`public_note`, g.`alt`, g.`online` " \ -" FROM `character_data` AS c LEFT JOIN `guild_members` AS g ON c.`id` = g.`char_id` " +" g.`banker`, g.`public_note`, g.`alt`, g.`online`, a.`offline` " \ +" FROM `character_data` AS c LEFT JOIN `guild_members` AS g ON c.`id` = g.`char_id` " \ +" LEFT JOIN `account` AS a ON a.`id` = c.`account_id` " + static void ProcessGuildMember(MySQLRequestRow row, CharGuildInfo &into) { //fields from `characer_` @@ -866,6 +868,7 @@ static void ProcessGuildMember(MySQLRequestRow row, CharGuildInfo &into) into.public_note = row[12] ? row[12] : ""; into.alt = row[13] ? (row[13][0] == '0' ? false : true) : false; into.online = row[14] ? (row[14][0] == '0' ? false : true) : false; + into.offline_mode = row[15] ? (row[15][0] == '0' ? false : true) : false; //a little sanity checking/cleanup if (into.guild_id == 0) { diff --git a/common/guild_base.h b/common/guild_base.h index 0cd4fe5f8..76887bb9c 100644 --- a/common/guild_base.h +++ b/common/guild_base.h @@ -55,16 +55,17 @@ class CharGuildInfo uint32 time_last_on; uint32 zone_id; - //fields from `guild_members` - uint32 guild_id; - uint8 rank; - bool tribute_enable; - uint32 total_tribute; - uint32 last_tribute; //timestamp - bool banker; - bool alt; - std::string public_note; - bool online; + // fields from `guild_members` + uint32 guild_id; + uint8 rank; + bool tribute_enable; + uint32 total_tribute; + uint32 last_tribute; // timestamp + bool banker; + bool alt; + std::string public_note; + bool online; + bool offline_mode; }; //this object holds guild functionality shared between world and zone. diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 407c021df..ce2891995 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -1845,7 +1845,7 @@ namespace RoF2 e->zoneinstance = 0; e->zone_id = htons(emu_e->zone_id); e->unknown_one2 = htonl(1); - e->unknown04 = 0; + e->offline_mode = htonl(emu_e->offline_mode); #undef SlideStructString #undef PutFieldN @@ -1862,14 +1862,12 @@ namespace RoF2 { SETUP_DIRECT_ENCODE(GuildMemberUpdate_Struct, structs::GuildMemberUpdate_Struct); - OUT(GuildID); - memcpy(eq->MemberName, emu->MemberName, sizeof(eq->MemberName)); - //OUT(ZoneID); - //OUT(InstanceID); - eq->InstanceID = emu->InstanceID; - eq->ZoneID = emu->ZoneID; - OUT(LastSeen); - eq->Unknown76 = 0; + eq->guild_id = emu->GuildID; + eq->last_seen = emu->LastSeen; + eq->instance_id = emu->InstanceID; + eq->zone_id = emu->ZoneID; + eq->offline_mode = emu->offline_mode; + memcpy(eq->member_name, emu->MemberName, sizeof(eq->member_name)); FINISH_ENCODE(); } @@ -4475,6 +4473,7 @@ namespace RoF2 *p = nullptr; char *InBuffer = (char *)in->pBuffer; + std::vector p_ids { 0x430, 0x420 }; WhoAllReturnStruct *wars = (WhoAllReturnStruct*)InBuffer; @@ -4500,8 +4499,9 @@ namespace RoF2 x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, x); - InBuffer += 4; - VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); + x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, std::ranges::find(p_ids.begin(), p_ids.end(), x) == p_ids.end() ? 0 : x); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0xffffffff); char Name[64]; @@ -4737,6 +4737,10 @@ namespace RoF2 OtherData = OtherData | 0x01; } + if (emu->offline) { + OtherData = OtherData | 0x02; + } + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, OtherData); // float EmitterScalingRadius diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 745ecc470..6b5f85215 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -3681,22 +3681,22 @@ struct SimpleMessage_Struct{ // Size: 52 + strings // Other than the strings, all of this packet is network byte order (reverse from normal) struct GuildMemberEntry_Struct { - char name[1]; // variable length - uint32 level; - uint32 banker; // 1=yes, 0=no - uint32 class_; - uint32 rank; - uint32 time_last_on; - uint32 tribute_enable; - uint32 unknown01; // Seen 0 - uint32 total_tribute; // total guild tribute donated, network byte order - uint32 last_tribute; // unix timestamp - uint32 unknown_one; // unknown, set to 1 - char public_note[1]; // variable length. - uint16 zoneinstance; // Seen 0s or -1 in RoF2 - uint16 zone_id; // Seen 0s or -1 in RoF2 - uint32 unknown_one2; // unknown, set to 1 - uint32 unknown04; // Seen 0 + char name[1]; // variable length + uint32 level; + uint32 banker; // 1=yes, 0=no + uint32 class_; + uint32 rank; + uint32 time_last_on; + uint32 tribute_enable; + uint32 unknown01; // Seen 0 + uint32 total_tribute; // total guild tribute donated, network byte order + uint32 last_tribute; // unix timestamp + uint32 unknown_one; // unknown, set to 1 + char public_note[1]; // variable length. + uint16 zoneinstance; // Seen 0s or -1 in RoF2 + uint16 zone_id; // Seen 0s or -1 in RoF2 + uint32 unknown_one2; // unknown, set to 1 + uint32 offline_mode; // Displays OFFLINE MODE instead of Zone Name }; //just for display purposes, this is not actually used in the message encoding other than for size. @@ -3731,13 +3731,12 @@ struct GuildStatus_Struct }; struct GuildMemberUpdate_Struct { -/*00*/ uint32 GuildID; -/*04*/ char MemberName[64]; -/*68*/ uint16 ZoneID; -/*70*/ uint16 InstanceID; //speculated -/*72*/ uint32 LastSeen; //unix timestamp -/*76*/ uint32 Unknown76; -/*80*/ + /*00*/ uint32 guild_id; + /*04*/ char member_name[64]; + /*68*/ uint16 zone_id; + /*70*/ uint16 instance_id; //speculated + /*72*/ uint32 last_seen; //unix timestamp + /*76*/ uint32 offline_mode; }; struct GuildMemberLevelUpdate_Struct { diff --git a/common/repositories/account_repository.h b/common/repositories/account_repository.h index 5f2b08581..9f269abb6 100644 --- a/common/repositories/account_repository.h +++ b/common/repositories/account_repository.h @@ -107,6 +107,45 @@ public: return AccountRepository::UpdateOne(db, e); } + + static void SetOfflineStatus(Database& db, const uint32 account_id, bool offline_status) + { + auto account = FindOne(db, account_id); + if (!account.id) { + return; + } + + account.offline = offline_status; + UpdateOne(db, account); + } + + static void ClearAllOfflineStatus(Database& db) + { + auto query = fmt::format("UPDATE {} SET `offline` = '0' WHERE `offline` = '1';", + TableName() + ); + + db.QueryDatabase(query); + } + + static bool GetAllOfflineStatus(Database& db, const uint32 character_id) + { + auto query = fmt::format("SELECT a.`offline` " + "FROM `account` AS a " + "INNER JOIN character_data AS c ON c.account_id = a.id " + "WHERE c.id = '{}'", + character_id + ); + auto results = db.QueryDatabase(query); + if (!results.Success() || !results.RowCount()) { + return false; + } + + auto row = results.begin(); + bool const status = static_cast(Strings::ToInt(row[0])); + + return status; + } }; #endif //EQEMU_ACCOUNT_REPOSITORY_H diff --git a/common/repositories/base/base_account_repository.h b/common/repositories/base/base_account_repository.h index 33b198f0e..d07603f89 100644 --- a/common/repositories/base/base_account_repository.h +++ b/common/repositories/base/base_account_repository.h @@ -39,6 +39,7 @@ public: uint8_t rulesflag; time_t suspendeduntil; uint32_t time_creation; + uint8_t offline; std::string ban_reason; std::string suspend_reason; std::string crc_eqgame; @@ -74,6 +75,7 @@ public: "rulesflag", "suspendeduntil", "time_creation", + "offline", "ban_reason", "suspend_reason", "crc_eqgame", @@ -105,6 +107,7 @@ public: "rulesflag", "UNIX_TIMESTAMP(suspendeduntil)", "time_creation", + "offline", "ban_reason", "suspend_reason", "crc_eqgame", @@ -170,6 +173,7 @@ public: e.rulesflag = 0; e.suspendeduntil = 0; e.time_creation = 0; + e.offline = 0; e.ban_reason = ""; e.suspend_reason = ""; e.crc_eqgame = ""; @@ -231,11 +235,12 @@ public: e.rulesflag = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; e.suspendeduntil = strtoll(row[18] ? row[18] : "-1", nullptr, 10); e.time_creation = row[19] ? static_cast(strtoul(row[19], nullptr, 10)) : 0; - e.ban_reason = row[20] ? row[20] : ""; - e.suspend_reason = row[21] ? row[21] : ""; - e.crc_eqgame = row[22] ? row[22] : ""; - e.crc_skillcaps = row[23] ? row[23] : ""; - e.crc_basedata = row[24] ? row[24] : ""; + e.offline = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.ban_reason = row[21] ? row[21] : ""; + e.suspend_reason = row[22] ? row[22] : ""; + e.crc_eqgame = row[23] ? row[23] : ""; + e.crc_skillcaps = row[24] ? row[24] : ""; + e.crc_basedata = row[25] ? row[25] : ""; return e; } @@ -288,11 +293,12 @@ public: v.push_back(columns[17] + " = " + std::to_string(e.rulesflag)); v.push_back(columns[18] + " = FROM_UNIXTIME(" + (e.suspendeduntil > 0 ? std::to_string(e.suspendeduntil) : "null") + ")"); v.push_back(columns[19] + " = " + std::to_string(e.time_creation)); - v.push_back(columns[20] + " = '" + Strings::Escape(e.ban_reason) + "'"); - v.push_back(columns[21] + " = '" + Strings::Escape(e.suspend_reason) + "'"); - v.push_back(columns[22] + " = '" + Strings::Escape(e.crc_eqgame) + "'"); - v.push_back(columns[23] + " = '" + Strings::Escape(e.crc_skillcaps) + "'"); - v.push_back(columns[24] + " = '" + Strings::Escape(e.crc_basedata) + "'"); + v.push_back(columns[20] + " = " + std::to_string(e.offline)); + v.push_back(columns[21] + " = '" + Strings::Escape(e.ban_reason) + "'"); + v.push_back(columns[22] + " = '" + Strings::Escape(e.suspend_reason) + "'"); + v.push_back(columns[23] + " = '" + Strings::Escape(e.crc_eqgame) + "'"); + v.push_back(columns[24] + " = '" + Strings::Escape(e.crc_skillcaps) + "'"); + v.push_back(columns[25] + " = '" + Strings::Escape(e.crc_basedata) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -334,6 +340,7 @@ public: v.push_back(std::to_string(e.rulesflag)); v.push_back("FROM_UNIXTIME(" + (e.suspendeduntil > 0 ? std::to_string(e.suspendeduntil) : "null") + ")"); v.push_back(std::to_string(e.time_creation)); + v.push_back(std::to_string(e.offline)); v.push_back("'" + Strings::Escape(e.ban_reason) + "'"); v.push_back("'" + Strings::Escape(e.suspend_reason) + "'"); v.push_back("'" + Strings::Escape(e.crc_eqgame) + "'"); @@ -388,6 +395,7 @@ public: v.push_back(std::to_string(e.rulesflag)); v.push_back("FROM_UNIXTIME(" + (e.suspendeduntil > 0 ? std::to_string(e.suspendeduntil) : "null") + ")"); v.push_back(std::to_string(e.time_creation)); + v.push_back(std::to_string(e.offline)); v.push_back("'" + Strings::Escape(e.ban_reason) + "'"); v.push_back("'" + Strings::Escape(e.suspend_reason) + "'"); v.push_back("'" + Strings::Escape(e.crc_eqgame) + "'"); @@ -446,11 +454,12 @@ public: e.rulesflag = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; e.suspendeduntil = strtoll(row[18] ? row[18] : "-1", nullptr, 10); e.time_creation = row[19] ? static_cast(strtoul(row[19], nullptr, 10)) : 0; - e.ban_reason = row[20] ? row[20] : ""; - e.suspend_reason = row[21] ? row[21] : ""; - e.crc_eqgame = row[22] ? row[22] : ""; - e.crc_skillcaps = row[23] ? row[23] : ""; - e.crc_basedata = row[24] ? row[24] : ""; + e.offline = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.ban_reason = row[21] ? row[21] : ""; + e.suspend_reason = row[22] ? row[22] : ""; + e.crc_eqgame = row[23] ? row[23] : ""; + e.crc_skillcaps = row[24] ? row[24] : ""; + e.crc_basedata = row[25] ? row[25] : ""; all_entries.push_back(e); } @@ -495,11 +504,12 @@ public: e.rulesflag = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; e.suspendeduntil = strtoll(row[18] ? row[18] : "-1", nullptr, 10); e.time_creation = row[19] ? static_cast(strtoul(row[19], nullptr, 10)) : 0; - e.ban_reason = row[20] ? row[20] : ""; - e.suspend_reason = row[21] ? row[21] : ""; - e.crc_eqgame = row[22] ? row[22] : ""; - e.crc_skillcaps = row[23] ? row[23] : ""; - e.crc_basedata = row[24] ? row[24] : ""; + e.offline = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.ban_reason = row[21] ? row[21] : ""; + e.suspend_reason = row[22] ? row[22] : ""; + e.crc_eqgame = row[23] ? row[23] : ""; + e.crc_skillcaps = row[24] ? row[24] : ""; + e.crc_basedata = row[25] ? row[25] : ""; all_entries.push_back(e); } @@ -594,6 +604,7 @@ public: v.push_back(std::to_string(e.rulesflag)); v.push_back("FROM_UNIXTIME(" + (e.suspendeduntil > 0 ? std::to_string(e.suspendeduntil) : "null") + ")"); v.push_back(std::to_string(e.time_creation)); + v.push_back(std::to_string(e.offline)); v.push_back("'" + Strings::Escape(e.ban_reason) + "'"); v.push_back("'" + Strings::Escape(e.suspend_reason) + "'"); v.push_back("'" + Strings::Escape(e.crc_eqgame) + "'"); @@ -641,6 +652,7 @@ public: v.push_back(std::to_string(e.rulesflag)); v.push_back("FROM_UNIXTIME(" + (e.suspendeduntil > 0 ? std::to_string(e.suspendeduntil) : "null") + ")"); v.push_back(std::to_string(e.time_creation)); + v.push_back(std::to_string(e.offline)); v.push_back("'" + Strings::Escape(e.ban_reason) + "'"); v.push_back("'" + Strings::Escape(e.suspend_reason) + "'"); v.push_back("'" + Strings::Escape(e.crc_eqgame) + "'"); diff --git a/common/repositories/base/base_character_offline_transactions_repository.h b/common/repositories/base/base_character_offline_transactions_repository.h new file mode 100644 index 000000000..64141e6d0 --- /dev/null +++ b/common/repositories/base/base_character_offline_transactions_repository.h @@ -0,0 +1,451 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H +#define EQEMU_BASE_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseCharacterOfflineTransactionsRepository { +public: + struct CharacterOfflineTransactions { + uint64_t id; + uint32_t character_id; + uint32_t type; + std::string item_name; + int32_t quantity; + uint64_t price; + std::string buyer_name; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "character_id", + "type", + "item_name", + "quantity", + "price", + "buyer_name", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "character_id", + "type", + "item_name", + "quantity", + "price", + "buyer_name", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("character_offline_transactions"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static CharacterOfflineTransactions NewEntity() + { + CharacterOfflineTransactions e{}; + + e.id = 0; + e.character_id = 0; + e.type = 0; + e.item_name = ""; + e.quantity = 0; + e.price = 0; + e.buyer_name = ""; + + return e; + } + + static CharacterOfflineTransactions GetCharacterOfflineTransactions( + const std::vector &character_offline_transactionss, + int character_offline_transactions_id + ) + { + for (auto &character_offline_transactions : character_offline_transactionss) { + if (character_offline_transactions.id == character_offline_transactions_id) { + return character_offline_transactions; + } + } + + return NewEntity(); + } + + static CharacterOfflineTransactions FindOne( + Database& db, + int character_offline_transactions_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + character_offline_transactions_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + CharacterOfflineTransactions e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.type = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_name = row[3] ? row[3] : ""; + e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; + e.price = row[5] ? strtoull(row[5], nullptr, 10) : 0; + e.buyer_name = row[6] ? row[6] : ""; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int character_offline_transactions_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + character_offline_transactions_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const CharacterOfflineTransactions &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.character_id)); + v.push_back(columns[2] + " = " + std::to_string(e.type)); + v.push_back(columns[3] + " = '" + Strings::Escape(e.item_name) + "'"); + v.push_back(columns[4] + " = " + std::to_string(e.quantity)); + v.push_back(columns[5] + " = " + std::to_string(e.price)); + v.push_back(columns[6] + " = '" + Strings::Escape(e.buyer_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static CharacterOfflineTransactions InsertOne( + Database& db, + CharacterOfflineTransactions e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.type)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + v.push_back(std::to_string(e.quantity)); + v.push_back(std::to_string(e.price)); + v.push_back("'" + Strings::Escape(e.buyer_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.type)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + v.push_back(std::to_string(e.quantity)); + v.push_back(std::to_string(e.price)); + v.push_back("'" + Strings::Escape(e.buyer_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + CharacterOfflineTransactions e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.type = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_name = row[3] ? row[3] : ""; + e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; + e.price = row[5] ? strtoull(row[5], nullptr, 10) : 0; + e.buyer_name = row[6] ? row[6] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + CharacterOfflineTransactions e{}; + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.type = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_name = row[3] ? row[3] : ""; + e.quantity = row[4] ? static_cast(atoi(row[4])) : 0; + e.price = row[5] ? strtoull(row[5], nullptr, 10) : 0; + e.buyer_name = row[6] ? row[6] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const CharacterOfflineTransactions &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.type)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + v.push_back(std::to_string(e.quantity)); + v.push_back(std::to_string(e.price)); + v.push_back("'" + Strings::Escape(e.buyer_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.type)); + v.push_back("'" + Strings::Escape(e.item_name) + "'"); + v.push_back(std::to_string(e.quantity)); + v.push_back(std::to_string(e.price)); + v.push_back("'" + Strings::Escape(e.buyer_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H diff --git a/common/repositories/buyer_repository.h b/common/repositories/buyer_repository.h index 2d402b0dc..579228456 100644 --- a/common/repositories/buyer_repository.h +++ b/common/repositories/buyer_repository.h @@ -106,8 +106,13 @@ public: return false; } - auto buy_lines = - BaseBuyerBuyLinesRepository::GetWhere(db, fmt::format("`buyer_id` = {}", buyer.front().id)); + auto buy_lines = BaseBuyerBuyLinesRepository::GetWhere( + db, + fmt::format("`buyer_id` = '{}'", buyer.front().id) + ); + if (buy_lines.empty()) { + return false; + } std::vector buy_line_ids{}; for (auto const &bl: buy_lines) { @@ -175,6 +180,26 @@ public: return true; } + + static bool UpdateBuyerEntityID(Database &db, uint32 char_id, uint32 old_entity_id, uint32 new_entity_id) + { + if (!char_id || !old_entity_id || !new_entity_id) { + return false; + } + + auto results = GetWhere(db, fmt::format("`char_id` = '{}' AND `char_entity_id` = '{}' LIMIT 1;", char_id, old_entity_id)); + + if (results.empty()) { + return false; + } + + for (auto &e: results) { + e.char_entity_id = new_entity_id; + } + + ReplaceMany(db, results); + return true; + } }; #endif //EQEMU_BUYER_REPOSITORY_H diff --git a/common/repositories/character_offline_transactions_repository.h b/common/repositories/character_offline_transactions_repository.h new file mode 100644 index 000000000..6802a4a4a --- /dev/null +++ b/common/repositories/character_offline_transactions_repository.h @@ -0,0 +1,53 @@ +#ifndef EQEMU_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H +#define EQEMU_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_character_offline_transactions_repository.h" + +class CharacterOfflineTransactionsRepository: public BaseCharacterOfflineTransactionsRepository { +public: + +#define TRADER_TRANSACTION 1 +#define BUYER_TRANSACTION 2 + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * CharacterOfflineTransactionsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * CharacterOfflineTransactionsRepository::GetWhereNeverExpires() + * CharacterOfflineTransactionsRepository::GetWhereXAndY() + * CharacterOfflineTransactionsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_CHARACTER_OFFLINE_TRANSACTIONS_REPOSITORY_H diff --git a/common/repositories/trader_repository.h b/common/repositories/trader_repository.h index e3dd79a32..5afb76c33 100644 --- a/common/repositories/trader_repository.h +++ b/common/repositories/trader_repository.h @@ -395,6 +395,49 @@ public: return all_entries; } + + static Trader GetAccountZoneIdAndInstanceIdByAccountId(Database &db, uint32 account_id) + { + auto trader_query = fmt::format( + "SELECT t.id, t.char_id, t.char_zone_id, t.char_zone_instance_id " + "FROM trader AS t " + "WHERE t.char_id IN(SELECT c.id FROM character_data AS c WHERE c.account_id = '{}') " + "LIMIT 1;", + account_id + ); + + auto buyer_query = fmt::format( + "SELECT t.id, t.char_id, t.char_zone_id, t.char_zone_instance_id " + "FROM buyer AS t " + "WHERE t.char_id IN(SELECT c.id FROM character_data AS c WHERE c.account_id = '{}') " + "LIMIT 1;", + account_id + ); + + Trader e{}; + + auto trader_results = db.QueryDatabase(trader_query); + auto buyer_results = db.QueryDatabase(buyer_query); + if (trader_results.RowCount() == 0 && buyer_results.RowCount() == 0) { + return e; + } + + MySQLRequestRow row; + if (trader_results.RowCount() > 0) { + row = trader_results.begin(); + } + + if (buyer_results.RowCount() > 0) { + row = buyer_results.begin(); + } + + e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.char_zone_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.char_zone_instance_id = row[3] ? static_cast(atoi(row[3])) : 0; + + return e; + } }; #endif //EQEMU_TRADER_REPOSITORY_H diff --git a/common/servertalk.h b/common/servertalk.h index 2180d9c85..791434305 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -229,10 +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_UsertoWorldReqLeg 0xAB00 +#define ServerOP_UsertoWorldRespLeg 0xAB01 +#define ServerOP_UsertoWorldReq 0xAB02 +#define ServerOP_UsertoWorldResp 0xAB03 +#define ServerOP_UsertoWorldCancelOfflineRequest 0xAB04 +#define ServerOP_UsertoWorldCancelOfflineResponse 0xAB05 #define ServerOP_LauncherConnectInfo 0x3000 #define ServerOP_LauncherZoneRequest 0x3001 @@ -360,12 +362,13 @@ enum { QSG_LFGuild_PlayerMatches = 0, QSG_LFGuild_UpdatePlayerInfo, QSG_LFGuild_ enum { - UserToWorldStatusWorldUnavail = 0, - UserToWorldStatusSuccess = 1, - UserToWorldStatusSuspended = -1, - UserToWorldStatusBanned = -2, - UserToWorldStatusWorldAtCapacity = -3, - UserToWorldStatusAlreadyOnline = -4 + UserToWorldStatusWorldUnavail = 0, + UserToWorldStatusSuccess = 1, + UserToWorldStatusSuspended = -1, + UserToWorldStatusBanned = -2, + UserToWorldStatusWorldAtCapacity = -3, + UserToWorldStatusAlreadyOnline = -4, + UserToWorldStatusOffilineTraderBuyer = -5 }; enum { @@ -567,6 +570,9 @@ struct ServerClientList_Struct { uint8 LFGToLevel; bool LFGMatchFilter; char LFGComments[64]; + bool trader; + bool buyer; + bool offline; }; struct ServerClientListKeepAlive_Struct { @@ -1030,6 +1036,7 @@ struct ServerGuildMemberUpdate_Struct { char member_name[64]; uint32 zone_id; uint32 last_seen; + uint32 offline_mode; }; struct ServerGuildPermissionUpdate_Struct { diff --git a/loginserver/client.cpp b/loginserver/client.cpp index 754c83ab5..20fbca94d 100644 --- a/loginserver/client.cpp +++ b/loginserver/client.cpp @@ -71,6 +71,25 @@ 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; @@ -561,3 +580,27 @@ std::string Client::GetClientLoggingDescription() client_ip ); } + +void Client::SendCancelOfflineStatusToWorld(const char *data) +{ + if (m_client_status != cs_logged_in) { + LogError("Client sent a play request when they were not logged in, discarding"); + return; + } + + const auto *play = (const PlayEverquestRequest *) data; + auto server_id_in = (unsigned int) play->server_number; + auto sequence_in = (unsigned int) play->base_header.sequence; + + LogLoginserverDetail( + "Step 2 - Cancel Offline Status Request received from client [{}] server number [{}] sequence [{}]", + GetAccountName(), + server_id_in, + sequence_in + ); + + m_selected_play_server_id = (unsigned int) play->server_number; + m_play_sequence_id = sequence_in; + m_selected_play_server_id = server_id_in; + server.server_manager->SendUserToWorldCancelOfflineRequest(server_id_in, m_account_id, m_loginserver_name); +} diff --git a/loginserver/client.h b/loginserver/client.h index 0dc71d00c..6e09a661e 100644 --- a/loginserver/client.h +++ b/loginserver/client.h @@ -27,6 +27,7 @@ public: void SendPlayToWorld(const char *data); void SendServerListPacket(uint32 seq); void SendPlayResponse(EQApplicationPacket *outapp); + void SendCancelOfflineStatusToWorld(const char *data); void GenerateRandomLoginKey(); unsigned int GetAccountID() const { return m_account_id; } std::string GetLoginServerName() const { return m_loginserver_name; } diff --git a/loginserver/login_types.h b/loginserver/login_types.h index 127514696..2c8541826 100644 --- a/loginserver/login_types.h +++ b/loginserver/login_types.h @@ -79,6 +79,11 @@ struct PlayEverquestResponse { uint32 server_number; }; +struct CancelOfflineTrader { + LoginBaseMessage base_header; + int16_t unk; +}; + #pragma pack() enum LSClientVersion { @@ -154,11 +159,12 @@ 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. - }; + }; } #endif diff --git a/loginserver/login_util/login_opcodes_sod.conf b/loginserver/login_util/login_opcodes_sod.conf index cdc856d2c..c91d144c5 100644 --- a/loginserver/login_util/login_opcodes_sod.conf +++ b/loginserver/login_util/login_opcodes_sod.conf @@ -11,3 +11,5 @@ OP_Poll=0x0029 OP_LoginExpansionPacketData=0x0031 OP_EnterChat=0x000f OP_PollResponse=0x0011 +OP_CancelOfflineTrader=0x0016 + OP_CancelOfflineTraderResponse=0x0030 \ No newline at end of file diff --git a/loginserver/world_server.cpp b/loginserver/world_server.cpp index 44e6049d1..507f38390 100644 --- a/loginserver/world_server.cpp +++ b/loginserver/world_server.cpp @@ -51,6 +51,12 @@ WorldServer::WorldServer(std::shared_ptr wo ServerOP_LSAccountUpdate, std::bind(&WorldServer::ProcessLSAccountUpdate, this, std::placeholders::_1, std::placeholders::_2) ); + + worldserver_connection->OnMessage( + ServerOP_UsertoWorldCancelOfflineResponse, + std::bind( + &WorldServer::ProcessUserToWorldCancelOfflineResponse, this, std::placeholders::_1, std::placeholders::_2) + ); } WorldServer::~WorldServer() = default; @@ -298,6 +304,10 @@ 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; @@ -774,3 +784,113 @@ void WorldServer::FormatWorldServerName(char *name, int8 server_list_type) strn0cpy(name, server_long_name.c_str(), 201); } + +void WorldServer::ProcessUserToWorldCancelOfflineResponse(uint16_t opcode, const EQ::Net::Packet &packet) + { + LogNetcode( + "Application packet received from server [{:#04x}] [Size: {}]\n{}", + opcode, + packet.Length(), + packet.ToString() + ); + LogLoginserverDetail("Step 8 - back in Login Server from world."); + + if (packet.Length() < sizeof(UsertoWorldResponse)) { + LogError( + "Received application packet from server that had opcode ServerOP_UsertoWorldCancelOfflineResp, " + "but was too small. Discarded to avoid buffer overrun" + ); + return; + } + + auto res = (UsertoWorldResponse *) packet.Data(); + LogDebug("Trying to find client with user id of [{}]", res->lsaccountid); + + Client *c = server.client_manager->GetClient( + res->lsaccountid, + res->login + ); + + if (c) { + LogDebug( + "Found client with user id of [{}] and account name of {}", + res->lsaccountid, + c->GetAccountName().c_str() + ); + + auto client_packet = EQApplicationPacket(OP_CancelOfflineTraderResponse, sizeof(PlayEverquestResponse)); + auto client_packet_payload = reinterpret_cast(client_packet.pBuffer); + + client_packet_payload->base_header.sequence = c->GetCurrentPlaySequence(); + client_packet_payload->server_number = c->GetSelectedPlayServerID(); + + LogLoginserverDetail( + "Step 9 - Send Play Response OPCODE 30 to remove the client message about having an offline Trader/Buyer" + ); + c->SendPlayResponse(&client_packet); + + auto outapp = new EQApplicationPacket(OP_PlayEverquestResponse, sizeof(PlayEverquestResponse)); + auto r = reinterpret_cast(outapp->pBuffer); + r->base_header.sequence = c->GetCurrentPlaySequence(); + r->server_number = c->GetSelectedPlayServerID(); + + LogDebug( + "Found sequence and play of [{}] [{}]", + c->GetCurrentPlaySequence(), + c->GetSelectedPlayServerID() + ); + + //LogDebug("[Size: [{}]] {}", outapp->size, DumpPacketToString(outapp)); + + if (res->response > 0) { + r->base_reply.success = true; + SendClientAuthToWorld(c); + } + + switch (res->response) { + case UserToWorldStatusSuccess: + r->base_reply.error_str_id = LS::ErrStr::ERROR_NONE; + break; + case UserToWorldStatusWorldUnavail: + r->base_reply.error_str_id = LS::ErrStr::ERROR_SERVER_UNAVAILABLE; + break; + case UserToWorldStatusSuspended: + r->base_reply.error_str_id = LS::ErrStr::ERROR_ACCOUNT_SUSPENDED; + break; + case UserToWorldStatusBanned: + r->base_reply.error_str_id = LS::ErrStr::ERROR_ACCOUNT_BANNED; + break; + case UserToWorldStatusWorldAtCapacity: + r->base_reply.error_str_id = LS::ErrStr::ERROR_WORLD_MAX_CAPACITY; + break; + case UserToWorldStatusAlreadyOnline: + r->base_reply.error_str_id = LS::ErrStr::ERROR_ACTIVE_CHARACTER; + break; + case UserToWorldStatusOffilineTraderBuyer: + r->base_reply.success = false; + r->base_reply.error_str_id = LS::ErrStr::ERROR_OFFLINE_TRADER; + break; + default: + r->base_reply.error_str_id = LS::ErrStr::ERROR_UNKNOWN; + break; + } + + LogDebug( + "Sending play response with following data, allowed [{}], sequence {}, server number {}, message {}", + r->base_reply.success, + r->base_header.sequence, + r->server_number, + r->base_reply.error_str_id + ); + LogLoginserverDetail("Step 10 - Send Play Response EnterWorld to client"); + + c->SendPlayResponse(outapp); + delete outapp; + } + else { + LogError( + "Received User-To-World Response for [{}] but could not find the client referenced!.", + res->lsaccountid + ); + } + } \ No newline at end of file diff --git a/loginserver/world_server.h b/loginserver/world_server.h index f5120d38e..865a3d522 100644 --- a/loginserver/world_server.h +++ b/loginserver/world_server.h @@ -60,6 +60,7 @@ private: void ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Net::Packet &packet); void ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Packet &packet); void ProcessLSAccountUpdate(uint16_t opcode, const EQ::Net::Packet &packet); + void ProcessUserToWorldCancelOfflineResponse(uint16_t opcode, const EQ::Net::Packet &packet); std::shared_ptr m_connection; diff --git a/loginserver/world_server_manager.cpp b/loginserver/world_server_manager.cpp index f18b48537..f2a609b9a 100644 --- a/loginserver/world_server_manager.cpp +++ b/loginserver/world_server_manager.cpp @@ -216,3 +216,35 @@ const std::list> &WorldServerManager::GetWorldServe { return m_world_servers; } + +void WorldServerManager::SendUserToWorldCancelOfflineRequest( + unsigned int server_id, + unsigned int client_account_id, + const std::string &client_loginserver +) +{ + auto iter = std::find_if( + m_world_servers.begin(), m_world_servers.end(), + [&](const std::unique_ptr &server) { + return server->GetServerId() == server_id; + } + ); + + if (iter != m_world_servers.end()) { + EQ::Net::DynamicPacket outapp; + outapp.Resize(sizeof(UsertoWorldRequest)); + + auto *r = reinterpret_cast(outapp.Data()); + r->worldid = server_id; + r->lsaccountid = client_account_id; + strncpy(r->login, client_loginserver.c_str(), 64); + + LogLoginserverDetail("Step 3 - Sending ServerOP_UsertoWorldCancelOfflineRequest to world for client account id {}", client_account_id); + (*iter)->GetConnection()->Send(ServerOP_UsertoWorldCancelOfflineRequest, outapp); + + LogNetcode("[UsertoWorldRequest] [Size: {}]\n{}", outapp.Length(), outapp.ToString()); + } + else { + LogError("Client requested a user to world but supplied an invalid id of {}", server_id); + } +} \ No newline at end of file diff --git a/loginserver/world_server_manager.h b/loginserver/world_server_manager.h index 711bc7560..de5cb74d2 100644 --- a/loginserver/world_server_manager.h +++ b/loginserver/world_server_manager.h @@ -18,6 +18,11 @@ public: unsigned int client_account_id, const std::string &client_loginserver ); + void SendUserToWorldCancelOfflineRequest( + unsigned int server_id, + unsigned int client_account_id, + const std::string &client_loginserver + ); std::unique_ptr CreateServerListPacket(Client *client, uint32 sequence); bool DoesServerExist(const std::string &s, const std::string &server_short_name, WorldServer *ignore = nullptr); void DestroyServerByName(std::string s, std::string server_short_name, WorldServer *ignore = nullptr); diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 3533aaa87..f01d7d762 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -749,3 +749,6 @@ OP_ChangePetName=0x5dab OP_InvokeNameChangeImmediate=0x4fe2 OP_InvokeNameChangeLazy=0x2f2e + +#Offline Trading Mode + OP_Offline=0x53d3 \ No newline at end of file diff --git a/world/cliententry.cpp b/world/cliententry.cpp index d6feb68dd..cf803c2ce 100644 --- a/world/cliententry.cpp +++ b/world/cliententry.cpp @@ -202,6 +202,9 @@ void ClientListEntry::Update(ZoneServer *iZS, ServerClientList_Struct *scl, CLE_ m_lfg = scl->LFG; m_gm = scl->gm; m_client_version = scl->ClientVersion; + m_trader = scl->trader; + m_buyer = scl->buyer; + m_offline = scl->offline; // Fields from the LFG Window if ((scl->LFGFromLevel != 0) && (scl->LFGToLevel != 0)) { @@ -219,6 +222,10 @@ void ClientListEntry::LeavingZone(ZoneServer *iZS, CLE_Status iOnline) if (iZS != 0 && iZS != m_zone_server) { return; } + + m_trader = false; + m_buyer = false; + m_offline = false; SetOnline(iOnline); SharedTaskManager::Instance()->RemoveActiveInvitationByCharacterID(CharID()); @@ -260,6 +267,10 @@ void ClientListEntry::ClearVars(bool iAll) m_lfg = 0; m_gm = 0; m_client_version = 0; + m_trader = false; + m_buyer = false; + m_offline = false; + for (auto &elem: m_tell_queue) { safe_delete_array(elem); } diff --git a/world/cliententry.h b/world/cliententry.h index bf9a2662c..508f98148 100644 --- a/world/cliententry.h +++ b/world/cliententry.h @@ -14,7 +14,8 @@ typedef enum { Online, CharSelect, Zoning, - InZone + InZone, + OfflineMode } CLE_Status; static const char *CLEStatusString[] = { @@ -23,7 +24,8 @@ static const char *CLEStatusString[] = { "Online", "CharSelect", "Zoning", - "InZone" + "InZone", + "OfflineMode" }; class ZoneServer; @@ -103,6 +105,10 @@ public: inline bool GetLFGMatchFilter() const { return m_lfg_match_filter; } inline const char *GetLFGComments() const { return m_lfg_comments; } inline uint8 GetClientVersion() { return m_client_version; } + bool GetTrader() const { return m_trader; } + bool GetBuyer() const { return m_buyer; } + bool GetOfflineMode() const { return m_offline; } + void SetOfflineMode(bool status) { m_offline = status; } inline bool TellQueueFull() const { return m_tell_queue.size() >= RuleI(World, TellQueueSize); } inline bool TellQueueEmpty() const { return m_tell_queue.empty(); } @@ -135,25 +141,28 @@ private: // Character info ZoneServer *m_zone_server{}; - uint32 m_zone{}; - uint16 m_instance{}; - uint32 m_char_id{}; - char m_char_name[64]{}; - uint8 m_level{}; - uint8 m_class_{}; - uint16 m_race{}; - uint8 m_anon{}; - uint8 m_tells_off{}; - uint32 m_guild_id{}; - uint32 m_guild_rank; - bool m_guild_tribute_opt_in{}; - bool m_lfg{}; - uint8 m_gm{}; - uint8 m_client_version{}; - uint8 m_lfg_from_level{}; - uint8 m_lfg_to_level{}; - bool m_lfg_match_filter{}; - char m_lfg_comments[64]{}; + uint32 m_zone{}; + uint16 m_instance{}; + uint32 m_char_id{}; + char m_char_name[64]{}; + uint8 m_level{}; + uint8 m_class_{}; + uint16 m_race{}; + uint8 m_anon{}; + uint8 m_tells_off{}; + uint32 m_guild_id{}; + uint32 m_guild_rank; + bool m_guild_tribute_opt_in{}; + bool m_lfg{}; + uint8 m_gm{}; + uint8 m_client_version{}; + uint8 m_lfg_from_level{}; + uint8 m_lfg_to_level{}; + bool m_lfg_match_filter{}; + char m_lfg_comments[64]{}; + bool m_trader = false; + bool m_buyer = false; + bool m_offline = false; // Tell Queue -- really a vector :D std::vector m_tell_queue; diff --git a/world/clientlist.cpp b/world/clientlist.cpp index d8f1d9fbc..7b8e99d6f 100644 --- a/world/clientlist.cpp +++ b/world/clientlist.cpp @@ -427,7 +427,10 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s while (iterator.MoreElements()) { if (iterator.GetData()->GetID() == scl->wid) { cle = iterator.GetData(); - if (scl->remove == 2) { + if (scl->remove == 3) { + cle->Update(zoneserver, scl, CLE_Status::OfflineMode); + } + else if (scl->remove == 2) { cle->LeavingZone(zoneserver, CLE_Status::Offline); } else if (scl->remove == 1) { @@ -441,7 +444,11 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s } iterator.Advance(); } - if (scl->remove == 2) { + + if (scl->remove == 3) { + cle = new ClientListEntry(GetNextCLEID(), zoneserver, scl, CLE_Status::OfflineMode); + } + else if (scl->remove == 2) { cle = new ClientListEntry(GetNextCLEID(), zoneserver, scl, CLE_Status::Online); } else if (scl->remove == 1) { @@ -479,7 +486,10 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s " LFGFromLevel [{}]" " LFGToLevel [{}]" " LFGMatchFilter [{}]" - " LFGComments [{}]", + " LFGComments [{}]" + " Trader [{}]" + " Buyer [{}]" + " Offline [{}]", scl->remove, scl->wid, scl->IP, @@ -506,7 +516,10 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s scl->LFGFromLevel, scl->LFGToLevel, scl->LFGMatchFilter, - scl->LFGComments + scl->LFGComments, + scl->trader, + scl->buyer, + scl->offline ); clientlist.Insert(cle); @@ -784,7 +797,14 @@ void ClientList::SendWhoAll(uint32 fromid,const char* to, int16 admin, Who_All_S rankstring = 0; iterator.Advance(); continue; - } else if (cle->GetGM()) { + } + else if (cle->GetTrader()) { + rankstring = 12315; + } + else if (cle->GetBuyer()) { + rankstring = 6056; + } + else if (cle->GetGM()) { if (cle->Admin() >= AccountStatus::GMImpossible) { rankstring = 5021; } else if (cle->Admin() >= AccountStatus::GMMgmt) { @@ -877,6 +897,18 @@ void ClientList::SendWhoAll(uint32 fromid,const char* to, int16 admin, Who_All_S strcpy(placcount,cle->AccountName()); } + if (cle->GetOfflineMode()) { + if (cle->GetTrader()) { + pidstring = 0x0430; + rankstring = 0xFFFFFFFF; + } + + if (cle->GetBuyer()) { + pidstring = 0x0420; + rankstring = 0xFFFFFFFF; + } + } + memcpy(bufptr,&formatstring, sizeof(uint32)); bufptr+=sizeof(uint32); memcpy(bufptr,&pidstring, sizeof(uint32)); @@ -1631,25 +1663,29 @@ void ClientList::OnTick(EQ::Timer *t) outclient["Server"] = Json::Value(); } - outclient["CharID"] = cle->CharID(); - outclient["name"] = cle->name(); - outclient["zone"] = cle->zone(); - outclient["instance"] = cle->instance(); - outclient["level"] = cle->level(); - outclient["class_"] = cle->class_(); - outclient["race"] = cle->race(); - outclient["Anon"] = cle->Anon(); + outclient["CharID"] = cle->CharID(); + outclient["name"] = cle->name(); + outclient["zone"] = cle->zone(); + outclient["instance"] = cle->instance(); + outclient["level"] = cle->level(); + outclient["class_"] = cle->class_(); + outclient["race"] = cle->race(); + outclient["Anon"] = cle->Anon(); - outclient["TellsOff"] = cle->TellsOff(); - outclient["GuildID"] = cle->GuildID(); - outclient["LFG"] = cle->LFG(); - outclient["GM"] = cle->GetGM(); - outclient["LocalClient"] = cle->IsLocalClient(); - outclient["LFGFromLevel"] = cle->GetLFGFromLevel(); - outclient["LFGToLevel"] = cle->GetLFGToLevel(); + outclient["TellsOff"] = cle->TellsOff(); + outclient["GuildID"] = cle->GuildID(); + outclient["LFG"] = cle->LFG(); + outclient["GM"] = cle->GetGM(); + outclient["LocalClient"] = cle->IsLocalClient(); + outclient["LFGFromLevel"] = cle->GetLFGFromLevel(); + outclient["LFGToLevel"] = cle->GetLFGToLevel(); outclient["LFGMatchFilter"] = cle->GetLFGMatchFilter(); - outclient["LFGComments"] = cle->GetLFGComments(); - outclient["ClientVersion"] = cle->GetClientVersion(); + outclient["LFGComments"] = cle->GetLFGComments(); + outclient["ClientVersion"] = cle->GetClientVersion(); + outclient["Trader"] = cle->GetTrader(); + outclient["Buyer"] = cle->GetBuyer(); + outclient["OfflineMode"] = cle->GetOfflineMode(); + out["data"].append(outclient); Iterator.Advance(); diff --git a/world/console.cpp b/world/console.cpp index 41906239b..5f8d03aa4 100644 --- a/world/console.cpp +++ b/world/console.cpp @@ -56,7 +56,7 @@ struct EQ::Net::ConsoleLoginStatus CheckLogin(const std::string &username, const const std::string& account_name = database.GetAccountName(ret.account_id); ret.account_name = account_name; - ret.status = database.GetAccountStatus(ret.account_id); + ret.status = database.GetAccountStatus(ret.account_id).status; return ret; } diff --git a/world/login_server.cpp b/world/login_server.cpp index 9bdc0c470..00aaa0a6e 100644 --- a/world/login_server.cpp +++ b/world/login_server.cpp @@ -1,24 +1,28 @@ -#include "../common/global_define.h" -#include -#include -#include -#include -#include -#include "../common/version.h" -#include "../common/servertalk.h" -#include "../common/misc_functions.h" -#include "../common/eq_packet_structs.h" -#include "../common/packet_dump.h" -#include "../common/strings.h" -#include "../common/eqemu_logsys.h" #include "login_server.h" +#include +#include +#include +#include +#include +#include "../common/eq_packet_structs.h" +#include "../common/eqemu_logsys.h" +#include "../common/global_define.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/trader_repository.h" +#include "../common/servertalk.h" +#include "../common/strings.h" +#include "../common/version.h" +#include "cliententry.h" +#include "clientlist.h" #include "login_server_list.h" -#include "zoneserver.h" +#include "world_config.h" #include "worlddb.h" #include "zonelist.h" -#include "clientlist.h" -#include "cliententry.h" -#include "world_config.h" +#include "zoneserver.h" extern uint32 numzones; extern uint32 numplayers; @@ -44,9 +48,9 @@ void LoginServer::ProcessUsertoWorldReqLeg(uint16_t opcode, EQ::Net::Packet &p) const WorldConfig *Config = WorldConfig::get(); LogNetcode("Received ServerPacket from LS OpCode {:#04x}", opcode); - UsertoWorldRequestLegacy *utwr = (UsertoWorldRequestLegacy *) p.Data(); - uint32 id = database.GetAccountIDFromLSID("eqemu", utwr->lsaccountid); - int16 status = database.GetAccountStatus(id); + UsertoWorldRequestLegacy *utwr = (UsertoWorldRequestLegacy *) p.Data(); + uint32 id = database.GetAccountIDFromLSID("eqemu", utwr->lsaccountid); + int16 status = database.GetAccountStatus(id).status; LogDebug( "id [{}] status [{}] account_id [{}] world_id [{}] from_id [{}] to_id [{}] ip [{}]", @@ -124,14 +128,19 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet &p) const WorldConfig *Config = WorldConfig::get(); LogNetcode("Received ServerPacket from LS OpCode {:#04x}", opcode); - UsertoWorldRequest *utwr = (UsertoWorldRequest *) p.Data(); - uint32 id = database.GetAccountIDFromLSID(utwr->login, utwr->lsaccountid); - int16 status = database.GetAccountStatus(id); + UsertoWorldRequest *utwr = (UsertoWorldRequest *) p.Data(); + uint32 id = database.GetAccountIDFromLSID(utwr->login, utwr->lsaccountid); + auto status_record = database.GetAccountStatus(id); + auto client = client_list.FindCLEByAccountID(id); + + if (client) { + client->SetOfflineMode(status_record.offline); + } LogDebug( "id [{}] status [{}] account_id [{}] world_id [{}] from_id [{}] to_id [{}] ip [{}]", id, - status, + status_record.status, utwr->lsaccountid, utwr->worldid, utwr->FromID, @@ -153,7 +162,7 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet &p) utwrs->response = UserToWorldStatusSuccess; if (Config->Locked == true) { - if (status < (RuleI(GM, MinStatusToBypassLockedServer))) { + if (status_record.status < (RuleI(GM, MinStatusToBypassLockedServer))) { LogDebug( "Server locked and status is not high enough for account_id [{0}]", utwr->lsaccountid @@ -165,27 +174,34 @@ void LoginServer::ProcessUsertoWorldReq(uint16_t opcode, EQ::Net::Packet &p) } int32 x = Config->MaxClients; - if ((int32) numplayers >= x && x != -1 && x != 255 && status < (RuleI(GM, MinStatusToBypassLockedServer))) { + if ((int32) numplayers >= x && x != -1 && x != 255 && status_record.status < (RuleI(GM, MinStatusToBypassLockedServer))) { LogDebug("World at capacity account_id [{0}]", utwr->lsaccountid); utwrs->response = UserToWorldStatusWorldAtCapacity; SendPacket(&outpack); return; } - if (status == -1) { + if (status_record.status == -1) { LogDebug("User suspended account_id [{0}]", utwr->lsaccountid); utwrs->response = UserToWorldStatusSuspended; SendPacket(&outpack); return; } - if (status == -2) { + if (status_record.status == -2) { LogDebug("User banned account_id [{0}]", utwr->lsaccountid); utwrs->response = UserToWorldStatusBanned; SendPacket(&outpack); return; } + if (status_record.offline) { + 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); @@ -573,6 +589,14 @@ bool LoginServer::Connect() std::placeholders::_2 ) ); + m_client->OnMessage( + ServerOP_UsertoWorldCancelOfflineRequest, + std::bind( + &LoginServer::ProcessUserToWorldCancelOfflineRequest, + this, + std::placeholders::_1, + std::placeholders::_2) + ); } return true; @@ -688,3 +712,87 @@ void LoginServer::SendAccountUpdate(ServerPacket *pack) } } +void LoginServer::ProcessUserToWorldCancelOfflineRequest(uint16_t opcode, EQ::Net::Packet &p) +{ + auto const Config = WorldConfig::get(); + LogNetcode("Received ServerPacket from LS OpCode {:#04x}", opcode); + + auto utwr = static_cast(p.Data()); + uint32 id = database.GetAccountIDFromLSID(utwr->login, utwr->lsaccountid); + auto status_record = database.GetAccountStatus(id); + + LogLoginserverDetail( + "Step 4 - World received CancelOfflineRequest for client login server account id {} offline mode {}", + id, + status_record.offline + ); + LogDebug( + "id [{}] status [{}] account_id [{}] world_id [{}] ip [{}]", + id, + status_record.status, + utwr->lsaccountid, + utwr->worldid, + utwr->IPAddr + ); + + ServerPacket server_packet; + server_packet.size = sizeof(UsertoWorldResponse); + server_packet.pBuffer = new uchar[server_packet.size]; + memset(server_packet.pBuffer, 0, server_packet.size); + + auto utwrs = reinterpret_cast(server_packet.pBuffer); + utwrs->lsaccountid = utwr->lsaccountid; + utwrs->ToID = utwr->FromID; + utwrs->worldid = utwr->worldid; + utwrs->response = UserToWorldStatusSuccess; + strn0cpy(utwrs->login, utwr->login, 64); + + if (Config->Locked == true) { + if (status_record.status < RuleI(GM, MinStatusToBypassLockedServer)) { + LogDebug("Server locked and status is not high enough for account_id [{0}]", utwr->lsaccountid); + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; + utwrs->response = UserToWorldStatusWorldUnavail; + SendPacket(&server_packet); + return; + } + } + + int32 x = Config->MaxClients; + if (static_cast(numplayers) >= x && + x != -1 && + x != 255 && + status_record.status < RuleI(GM, MinStatusToBypassLockedServer) + ) { + LogDebug("World at capacity account_id [{0}]", utwr->lsaccountid); + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; + utwrs->response = UserToWorldStatusWorldAtCapacity; + SendPacket(&server_packet); + return; + } + + auto trader = TraderRepository::GetAccountZoneIdAndInstanceIdByAccountId(database, id); + if (trader.id && + zoneserver_list.IsZoneBootedByZoneIdAndInstanceId(trader.char_zone_id, trader.char_zone_instance_id)) { + LogLoginserverDetail( + "Step 5a(1) - World Checked offline users zone/instance is booted. " + "Sending packet to zone id {} instance id {}", + trader.char_zone_id, + trader.char_zone_instance_id); + + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineRequest; + zoneserver_list.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."); + AccountRepository::SetOfflineStatus(database, id, false); + + LogLoginserverDetail("Step 5b(3) - World clearing trader and buyer tablese."); + TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", trader.id)); + BuyerRepository::DeleteBuyer(database, trader.id); + + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; + SendPacket(&server_packet); +} diff --git a/world/login_server.h b/world/login_server.h index 0a5e6a107..729f7a0b2 100644 --- a/world/login_server.h +++ b/world/login_server.h @@ -49,6 +49,7 @@ private: void ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet &p); void ProcessLSRemoteAddr(uint16_t opcode, EQ::Net::Packet &p); void ProcessLSAccountUpdate(uint16_t opcode, EQ::Net::Packet &p); + void ProcessUserToWorldCancelOfflineRequest(uint16_t opcode, EQ::Net::Packet &p); std::unique_ptr m_keepalive; diff --git a/world/zonelist.cpp b/world/zonelist.cpp index f108031dc..111748f97 100644 --- a/world/zonelist.cpp +++ b/world/zonelist.cpp @@ -1018,3 +1018,15 @@ void ZSList::QueueServerReload(ServerReload::Type &type) m_queued_reloads.emplace_back(type); m_queued_reloads_mutex.unlock(); } + +bool ZSList::IsZoneBootedByZoneIdAndInstanceId(uint32 zone_id, uint32 instance_id) const +{ + for (auto const& z : zone_server_list) { + auto r = z.get(); + if (r && r->GetZoneID() == zone_id && r->GetInstanceID() == instance_id) { + return true; + } + } + + return false; +} diff --git a/world/zonelist.h b/world/zonelist.h index 43dd52b44..757802eaf 100644 --- a/world/zonelist.h +++ b/world/zonelist.h @@ -34,6 +34,7 @@ public: bool SendPacketToZonesWithGMs(ServerPacket *pack); bool SendPacketToBootedZones(ServerPacket* pack); bool SetLockedZone(uint16 iZoneID, bool iLock); + bool IsZoneBootedByZoneIdAndInstanceId(uint32 zone_id, uint32 instance_id) const; EQTime worldclock; diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 4a372cb79..29afe2caf 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1726,9 +1726,34 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { break; } - default: - return; + default: { + break; + } } + break; + } + case ServerOP_UsertoWorldCancelOfflineResponse: { + auto utwr = reinterpret_cast(pack->pBuffer); + + 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 utwrs = reinterpret_cast(server_packet.pBuffer); + utwrs->lsaccountid = utwr->lsaccountid; + utwrs->ToID = utwr->FromID; + utwrs->worldid = utwr->worldid; + utwrs->response = UserToWorldStatusSuccess; + strn0cpy(utwrs->login, utwr->login, 64); + + LogLoginserverDetail( + "Step 7a - World received ServerOP_UsertoWorldCancelOfflineResponse back to login with success." + ); + + loginserverlist.SendPacket(&server_packet); + break; } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); diff --git a/zone/api_service.cpp b/zone/api_service.cpp index 607ab64c6..d4f91918f 100644 --- a/zone/api_service.cpp +++ b/zone/api_service.cpp @@ -59,7 +59,7 @@ EQ::Net::WebsocketLoginStatus CheckLogin( ret.account_name = database.GetAccountName(static_cast(ret.account_id)); ret.logged_in = true; - ret.status = database.GetAccountStatus(ret.account_id); + ret.status = database.GetAccountStatus(ret.account_id).status; return ret; } diff --git a/zone/client.cpp b/zone/client.cpp index 5b990635a..46c5db9f1 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -510,10 +510,10 @@ Client::Client(EQStreamInterface *ieqs) : Mob( client_data_loaded = false; berserk = false; dead = false; - eqs = ieqs; - ip = eqs->GetRemoteIP(); - port = ntohs(eqs->GetRemotePort()); - client_state = CLIENT_CONNECTING; + eqs = ieqs ? ieqs : nullptr; + ip = eqs ? eqs->GetRemoteIP() : 0; + port = eqs ? ntohs(eqs->GetRemotePort()) : 0; + client_state = eqs ? CLIENT_CONNECTING : CLIENT_CONNECTED; SetTrader(false); Haste = 0; SetCustomerID(0); @@ -700,6 +700,7 @@ Client::Client(EQStreamInterface *ieqs) : Mob( m_parcels.clear(); m_buyer_id = 0; + m_offline = false; SetBotPulling(false); SetBotPrecombat(false); @@ -728,10 +729,12 @@ Client::~Client() { zone->ClearEXPModifier(this); } - if (!IsZoning()) { - if(IsInAGuild()) { - guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + if (!IsZoning() && IsInAGuild()) { + guild_mgr.UpdateDbMemberOnline(CharacterID(), false); + if (IsOffline()) { + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), GetZoneID(), time(nullptr), 1); + } else { + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr), 0); } } @@ -743,11 +746,11 @@ Client::~Client() { if (merc) merc->Depop(); - if(IsTrader()) { + if(IsTrader() && !IsOffline()) { TraderEndTrader(); } - if(IsBuyer()) { + if(IsBuyer() && !IsOffline()) { ToggleBuyerMode(false); } @@ -779,7 +782,9 @@ Client::~Client() { if(isgrouped && !bZoning && is_zone_loaded) LeaveGroup(); - UpdateWho(2); + if (!IsOffline() && !IsTrader()) { + UpdateWho(2); + } if(IsHoveringForRespawn()) { @@ -2155,6 +2160,9 @@ void Client::UpdateWho(uint8 remove) s->race = GetRace(); s->class_ = GetClass(); s->level = GetLevel(); + s->trader = IsTrader(); + s->buyer = IsBuyer(); + s->offline = IsOffline(); if (m_pp.anon == 0) { s->anon = 0; @@ -2223,7 +2231,7 @@ void Client::FriendsWho(char *FriendsString) { void Client::UpdateAdmin(bool from_database) { int16 tmp = admin; if (from_database) { - admin = database.GetAccountStatus(account_id); + admin = database.GetAccountStatus(account_id).status; } if (tmp == admin && from_database) { @@ -2539,6 +2547,7 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) ns->spawn.guildID = GuildID(); ns->spawn.trader = IsTrader(); ns->spawn.buyer = IsBuyer(); + ns->spawn.offline = IsOffline(); // ns->spawn.linkdead = IsLD() ? 1 : 0; // ns->spawn.pvp = GetPVP(false) ? 1 : 0; ns->spawn.show_name = true; diff --git a/zone/client.h b/zone/client.h index 6a5ab378b..6d97b9031 100644 --- a/zone/client.h +++ b/zone/client.h @@ -416,6 +416,8 @@ public: int32 FindNextFreeParcelSlot(uint32 char_id); int32 FindNextFreeParcelSlotUsingMemory(); void SendParcelIconStatus(); + bool IsOffline() { return m_offline; } + void SetOffline(bool status) { m_offline = status; } void SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action); void SendBecomeTrader(BazaarTraderBarterActions action, uint32 trader_id); @@ -508,7 +510,12 @@ public: inline bool ClientDataLoaded() const { return client_data_loaded; } inline bool Connected() const { return (client_state == CLIENT_CONNECTED); } inline bool InZone() const { return (client_state == CLIENT_CONNECTED || client_state == CLIENT_LINKDEAD); } - inline void Disconnect() { eqs->Close(); client_state = DISCONNECTED; } + inline void Disconnect() { + if (eqs) { + eqs->Close(); + client_state = DISCONNECTED; + } + } inline bool IsLD() const { return (bool) (client_state == CLIENT_LINKDEAD); } void Kick(const std::string &reason); void WorldKick(); @@ -2117,6 +2124,7 @@ private: uint32 m_trader_count{}; std::map> m_trader_merchant_list{}; // itemid, qty, item_unique_id uint32 m_buyer_id; + bool m_offline; uint32 m_barter_time; int32 m_parcel_platinum; int32 m_parcel_gold; @@ -2446,6 +2454,68 @@ public: bool IsFilteredAFKPacket(const EQApplicationPacket *p); void CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p); void SyncWorldPositionsToClient(bool ignore_idle = false); + + + Mob* GetMob() { + return Mob::GetMob(); + } + + void Clone(Client& in) + { + WID = in.WID; + admin = in.admin; + guild_id = in.guild_id; + guildrank = in.guildrank; + LFG = in.LFG; + AFK = in.AFK; + trader_id = in.trader_id; + m_buyer_id = in.m_buyer_id; + race = in.race; + class_ = in.class_; + size = in.size; + deity = in.deity; + texture = in.texture; + m_ClientVersion = in.m_ClientVersion; + m_ClientVersionBit = in.m_ClientVersionBit; + character_id = in.character_id; + account_id = in.account_id; + lsaccountid = in.lsaccountid; + + m_pp.platinum = in.m_pp.platinum; + m_pp.gold = in.m_pp.gold; + m_pp.silver = in.m_pp.silver; + m_pp.copper = in.m_pp.copper; + m_pp.platinum_bank = in.m_pp.platinum_bank; + m_pp.gold_bank = in.m_pp.gold_bank; + m_pp.silver_bank = in.m_pp.silver_bank; + m_pp.copper_bank = in.m_pp.copper_bank; + m_pp.platinum_cursor = in.m_pp.platinum_cursor; + m_pp.gold_cursor = in.m_pp.gold_cursor; + m_pp.silver_cursor = in.m_pp.silver_cursor; + m_pp.copper_cursor = in.m_pp.copper_cursor; + m_pp.currentRadCrystals = in.m_pp.currentRadCrystals; + m_pp.careerRadCrystals = in.m_pp.careerRadCrystals; + m_pp.currentEbonCrystals = in.m_pp.currentEbonCrystals; + m_pp.careerEbonCrystals = in.m_pp.careerEbonCrystals; + m_pp.gm = in.m_pp.gm; + + m_inv.SetInventoryVersion(in.m_ClientVersion); + SetBodyType(in.GetBodyType(), false); + + for (auto [slot, item] : in.m_inv.GetPersonal()) { + if (item) { + m_inv.GetPersonal()[slot] = item->Clone(); + } + } + + for (auto [slot, item] : in.m_inv.GetWorn()) { + if (item) { + m_inv.GetWorn()[slot] = item->Clone(); + } + } + + CloneMob(*in.GetMob()); + } }; #endif diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 89b0d1d07..b51b0ee38 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -19,6 +19,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/eqemu_logsys.h" #include "../common/opcodemgr.h" #include "../common/raid.h" +#include "../common/repositories/character_offline_transactions_repository.h" #include #include @@ -323,6 +324,7 @@ void MapOpcodes() ConnectedOpcodes[OP_MoveCoin] = &Client::Handle_OP_MoveCoin; ConnectedOpcodes[OP_MoveItem] = &Client::Handle_OP_MoveItem; ConnectedOpcodes[OP_MoveMultipleItems] = &Client::Handle_OP_MoveMultipleItems; + ConnectedOpcodes[OP_Offline] = &Client::Handle_OP_Offline; ConnectedOpcodes[OP_OpenContainer] = &Client::Handle_OP_OpenContainer; ConnectedOpcodes[OP_OpenGuildTributeMaster] = &Client::Handle_OP_OpenGuildTributeMaster; ConnectedOpcodes[OP_OpenInventory] = &Client::Handle_OP_OpenInventory; @@ -857,6 +859,54 @@ void Client::CompleteConnect() } } + auto offline_transactions_trader = CharacterOfflineTransactionsRepository::GetWhere( + database, fmt::format("`character_id` = '{}' AND `type` = '{}'", CharacterID(), TRADER_TRANSACTION) + ); + if (offline_transactions_trader.size() > 0) { + Message(Chat::Yellow, "You sold the following items while in offline trader mode:"); + + for (auto const &t: offline_transactions_trader) { + Message( + Chat::Yellow, + fmt::format( + "You sold {} {}{} to {} for {}.", + t.quantity, + t.item_name, + t.quantity > 1 ? "s" : "", + t.buyer_name, + DetermineMoneyString(t.price)) + .c_str()); + } + + CharacterOfflineTransactionsRepository::DeleteWhere( + database, fmt::format("`character_id` = '{}' AND `type` = '{}'", CharacterID(), TRADER_TRANSACTION) + ); + } + + auto offline_transactions_buyer = CharacterOfflineTransactionsRepository::GetWhere( + database, fmt::format("`character_id` = '{}' AND `type` = '{}'", CharacterID(), BUYER_TRANSACTION) + ); + if (offline_transactions_buyer.size() > 0) { + Message(Chat::Yellow, "You bought the following items while in offline buyer mode:"); + + for (auto const &t: offline_transactions_buyer) { + Message( + Chat::Yellow, + fmt::format( + "You bought {} {}{} from {} for {}.", + t.quantity, + t.item_name, + t.quantity > 1 ? "s" : "", + t.buyer_name, + DetermineMoneyString(t.price)) + .c_str()); + } + + CharacterOfflineTransactionsRepository::DeleteWhere( + database, fmt::format("`character_id` = '{}' AND `type` = '{}'", CharacterID(), BUYER_TRANSACTION) + ); + } + if(ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { SendParcelStatus(); } @@ -900,7 +950,7 @@ void Client::CompleteConnect() SendGuildMembersList(); } - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), zone->GetZoneID(), time(nullptr)); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), zone->GetZoneID(), time(nullptr), 0); SendGuildList(); if (GetGuildListDirty()) { @@ -17360,3 +17410,51 @@ void Client::SyncWorldPositionsToClient(bool ignore_idle) m_is_idle = false; } } + + +void Client::Handle_OP_Offline(const EQApplicationPacket *app) +{ + if (IsThereACustomer()) { + auto customer = entity_list.GetClientByID(GetCustomerID()); + if (customer) { + auto end_session = new EQApplicationPacket(OP_ShopEnd); + customer->FastQueuePacket(&end_session); + } + } + + AccountRepository::SetOfflineStatus(database, AccountID(), true); + SetOffline(true); + + EQStreamInterface *eqsi = nullptr; + auto offline_client = new Client(eqsi); + + database.LoadCharacterData(CharacterID(), &offline_client->GetPP(), &offline_client->GetEPP()); + offline_client->Clone(*this); + offline_client->GetInv().SetGMInventory(true); + offline_client->SetPosition(GetX(), GetY(), GetZ()); + offline_client->SetHeading(GetHeading()); + offline_client->SetSpawned(); + offline_client->SetBecomeNPC(false); + offline_client->SetOffline(true); + entity_list.AddClient(offline_client); + + if (IsBuyer()) { + offline_client->SetBuyerID(offline_client->CharacterID()); + if (!BuyerRepository::UpdateBuyerEntityID(database, CharacterID(), GetID(), offline_client->GetID())) { + entity_list.RemoveMob(offline_client->CastToMob()->GetID()); + return; + } + } + else { + offline_client->SetTrader(true); + } + + OnDisconnect(true); + + auto outapp = new EQApplicationPacket(); + offline_client->CreateSpawnPacket(outapp); + entity_list.QueueClients(nullptr, outapp, false); + safe_delete(outapp); + + offline_client->UpdateWho(3); +} diff --git a/zone/client_packet.h b/zone/client_packet.h index 0840f5edc..8c606d1ee 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -226,6 +226,7 @@ void Handle_OP_MoveCoin(const EQApplicationPacket *app); void Handle_OP_MoveItem(const EQApplicationPacket *app); void Handle_OP_MoveMultipleItems(const EQApplicationPacket *app); + void Handle_OP_Offline(const EQApplicationPacket *app); void Handle_OP_OpenContainer(const EQApplicationPacket *app); void Handle_OP_OpenGuildTributeMaster(const EQApplicationPacket *app); void Handle_OP_OpenInventory(const EQApplicationPacket *app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index e14985763..ee7d8272b 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -178,7 +178,7 @@ bool Client::Process() { } if (IsInAGuild()) { guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr), 0); } SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Offline); @@ -207,7 +207,7 @@ bool Client::Process() { Save(); if (IsInAGuild()) { guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr), 0); } if (GetMerc()) @@ -588,7 +588,7 @@ bool Client::Process() { return false; } - if (client_state != CLIENT_LINKDEAD && !eqs->CheckState(ESTABLISHED)) { + if (eqs && client_state != CLIENT_LINKDEAD && !eqs->CheckState(ESTABLISHED)) { OnDisconnect(true); LogInfo("Client linkdead: {}", name); @@ -599,7 +599,7 @@ bool Client::Process() { } if (IsInAGuild()) { guild_mgr.UpdateDbMemberOnline(CharacterID(), false); - guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr)); + guild_mgr.SendGuildMemberUpdateToWorld(GetName(), GuildID(), 0, time(nullptr), 0); } return false; @@ -617,7 +617,7 @@ bool Client::Process() { /************ Get all packets from packet manager out queue and process them ************/ EQApplicationPacket *app = nullptr; - if (!eqs->CheckState(CLOSING)) + if (eqs && !eqs->CheckState(CLOSING)) { while (app = eqs->PopPacket()) { HandlePacket(app); @@ -627,7 +627,7 @@ bool Client::Process() { ClientToNpcAggroProcess(); - if (client_state != CLIENT_LINKDEAD && (client_state == CLIENT_ERROR || client_state == DISCONNECTED || client_state == CLIENT_KICKED || !eqs->CheckState(ESTABLISHED))) + if (eqs && client_state != CLIENT_LINKDEAD && (client_state == CLIENT_ERROR || client_state == DISCONNECTED || client_state == CLIENT_KICKED || !eqs->CheckState(ESTABLISHED))) { //client logged out or errored out //ResetTrade(); diff --git a/zone/entity.cpp b/zone/entity.cpp index 37fa9b4ad..ccd9c921e 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -5006,15 +5006,31 @@ void EntityList::ZoneWho(Client *c, Who_All_Struct *Who) strcpy(WAPP1->Name, ClientEntry->GetName()); Buffer += sizeof(WhoAllPlayerPart1) + strlen(WAPP1->Name); WhoAllPlayerPart2* WAPP2 = (WhoAllPlayerPart2*)Buffer; + WAPP2->RankMSGID = 0xFFFFFFFF; - if (ClientEntry->IsTrader()) - WAPP2->RankMSGID = 12315; - else if (ClientEntry->IsBuyer()) - WAPP2->RankMSGID = 6056; - else if (ClientEntry->Admin() >= AccountStatus::Steward && ClientEntry->GetGM()) + if (ClientEntry->IsOffline()) { + if (ClientEntry->IsTrader()) { + WAPP1->PIDMSGID = 0x0430; + } + if (ClientEntry->IsBuyer()) { + WAPP1->PIDMSGID = 0x0420; + } + } + else { + if (ClientEntry->IsTrader()) { + WAPP2->RankMSGID = 12315; + } + else if (ClientEntry->IsBuyer()) { + WAPP2->RankMSGID = 6056; + } + } + + if (ClientEntry->Admin() >= AccountStatus::Steward && ClientEntry->GetGM()) { WAPP2->RankMSGID = 12312; - else + } + else { WAPP2->RankMSGID = 0xFFFFFFFF; + } strcpy(WAPP2->Guild, GuildName.c_str()); Buffer += sizeof(WhoAllPlayerPart2) + strlen(WAPP2->Guild); diff --git a/zone/guild_mgr.cpp b/zone/guild_mgr.cpp index aa2d9fd70..4f167cc45 100644 --- a/zone/guild_mgr.cpp +++ b/zone/guild_mgr.cpp @@ -426,10 +426,11 @@ void ZoneGuildManager::ProcessWorldPacket(ServerPacket *pack) auto outapp = new EQApplicationPacket(OP_GuildMemberUpdate, sizeof(GuildMemberUpdate_Struct)); auto gmus = (GuildMemberUpdate_Struct *) outapp->pBuffer; - gmus->GuildID = sgmus->guild_id; - gmus->ZoneID = sgmus->zone_id; - gmus->InstanceID = 0; - gmus->LastSeen = sgmus->last_seen; + gmus->GuildID = sgmus->guild_id; + gmus->ZoneID = sgmus->zone_id; + gmus->InstanceID = 0; + gmus->LastSeen = sgmus->last_seen; + gmus->offline_mode = sgmus->offline_mode; strn0cpy(gmus->MemberName, sgmus->member_name, sizeof(gmus->MemberName)); entity_list.QueueClientsGuild(outapp, sgmus->guild_id); @@ -652,17 +653,24 @@ void ZoneGuildManager::ProcessWorldPacket(ServerPacket *pack) } } -void ZoneGuildManager::SendGuildMemberUpdateToWorld(const char *MemberName, uint32 GuildID, uint16 ZoneID, uint32 LastSeen) +void ZoneGuildManager::SendGuildMemberUpdateToWorld( + const char *MemberName, + uint32 GuildID, + uint16 ZoneID, + uint32 LastSeen, + uint32 offline_mode +) { auto pack = new ServerPacket(ServerOP_GuildMemberUpdate, sizeof(ServerGuildMemberUpdate_Struct)); - ServerGuildMemberUpdate_Struct *sgmus = (ServerGuildMemberUpdate_Struct*)pack->pBuffer; - sgmus->guild_id = GuildID; + auto sgmus = (ServerGuildMemberUpdate_Struct *) pack->pBuffer; + sgmus->guild_id = GuildID; + sgmus->zone_id = ZoneID; + sgmus->last_seen = LastSeen; + sgmus->offline_mode = offline_mode; strn0cpy(sgmus->member_name, MemberName, sizeof(sgmus->member_name)); - sgmus->zone_id = ZoneID; - sgmus->last_seen = LastSeen; - worldserver.SendPacket(pack); + worldserver.SendPacket(pack); safe_delete(pack); } @@ -1517,14 +1525,12 @@ uint8* ZoneGuildManager::MakeGuildMembers(uint32 guild_id, const char* prefix_na PutField(total_tribute); PutField(last_tribute); SlideStructString(note_buf, ci->public_note); - //e->zoneinstance = 0; - if (ci->online) { - e->zone_id = ci->zone_id; //This routine, if there is a zone_id, will update the entire guild window (roster, notes, tribute) for online characters. + e->zone_id = 0; //If zone_id is 0 and we rely on the current world routine, the notes/tribute tabs are not updated for online characters. + e->offline_mode = 0; + if (ci->online || ci->offline_mode) { + e->zone_id = ci->zone_id; //This routine, if there is a zone_id, will update the entire guild window (roster, notes, tribute) for online characters. + e->offline_mode = ci->offline_mode; } - else { - e->zone_id = 0; //If zone_id is 0 and we rely on the current world routine, the notes/tribute tabs are not updated for online characters. - } - #undef SlideStructString #undef PutFieldN diff --git a/zone/guild_mgr.h b/zone/guild_mgr.h index 0b59bd1f6..57874a7f4 100644 --- a/zone/guild_mgr.h +++ b/zone/guild_mgr.h @@ -89,7 +89,7 @@ public: void RecordInvite(uint32 char_id, uint32 guild_id, uint8 rank); bool VerifyAndClearInvite(uint32 char_id, uint32 guild_id, uint8 rank); - void SendGuildMemberUpdateToWorld(const char *MemberName, uint32 GuildID, uint16 ZoneID, uint32 LastSeen); + void SendGuildMemberUpdateToWorld(const char *MemberName, uint32 GuildID, uint16 ZoneID, uint32 LastSeen, uint32 offline_mode); void RequestOnlineGuildMembers(uint32 FromID, uint32 GuildID); void UpdateRankPermission(uint32 gid, uint32 charid, uint32 fid, uint32 rank, uint32 value); void SendPermissionUpdate(uint32 guild_id, uint32 rank, uint32 function_id, uint32 value); diff --git a/zone/mob.h b/zone/mob.h index ca0abe268..80bdf9ba5 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1952,6 +1952,105 @@ private: void DoSpellInterrupt(uint16 spell_id, int32 mana_cost, int my_curmana); void HandleDoorOpen(); + + public: + Mob* GetMob() { return this; } + + void CloneMob(Mob& in) { + strn0cpy(name, in.name, 64); + strn0cpy(orig_name, in.orig_name, 64); + strn0cpy(lastname, in.lastname, 64); + current_hp = in.current_hp; + max_hp = in.max_hp; + base_hp = in.base_hp; + gender = in.gender; + race = in.race; + base_gender = in.base_gender; + base_race = in.race; + use_model = in.use_model; + class_ = in.class_; + bodytype = in.bodytype; + orig_bodytype = in.orig_bodytype; + deity = in.deity; + level = in.level; + orig_level = in.orig_level; + npctype_id = in.npctype_id; + size = in.size; + base_size = in.base_size; + runspeed = in.runspeed; + texture = in.texture; + helmtexture = in.helmtexture; + armtexture = in.armtexture; + bracertexture = in.bracertexture; + handtexture = in.handtexture; + legtexture = in.legtexture; + feettexture = in.feettexture; + multitexture = in.multitexture; + haircolor = in.haircolor; + beardcolor = in.beardcolor; + eyecolor1 = in.eyecolor1; + eyecolor2 = in.eyecolor2; + hairstyle = in.hairstyle; + luclinface = in.luclinface; + beard = in.beard; + drakkin_heritage = in.drakkin_heritage; + drakkin_tattoo = in.drakkin_tattoo; + drakkin_details = in.drakkin_details; + attack_speed = in.attack_speed; + attack_delay = in.attack_delay; + slow_mitigation = in.slow_mitigation; + findable = in.findable; + trackable = in.trackable; + has_shield_equipped = in.has_shield_equipped; + has_two_hand_blunt_equipped = in.has_two_hand_blunt_equipped; + has_two_hander_equipped = in.has_two_hander_equipped; + has_dual_weapons_equipped = in.has_dual_weapons_equipped; + can_facestab = in.can_facestab; + has_numhits = in.has_numhits; + has_MGB = in.has_MGB; + has_ProjectIllusion = in.has_ProjectIllusion; + SpellPowerDistanceMod = in.SpellPowerDistanceMod; + last_los_check = in.last_los_check; + aa_title = in.aa_title; + AC = in.AC; + ATK = in.ATK; + STR = in.STR; + STA = in.STA; + DEX = in.DEX; + AGI = in.AGI; + INT = in.INT; + WIS = in.WIS; + CHA = in.CHA; + MR = in.MR; + extra_haste = in.extra_haste; + bEnraged = in.bEnraged; + current_mana = in.current_mana; + max_mana = in.max_mana; + hp_regen = in.hp_regen; + hp_regen_per_second = in.hp_regen_per_second; + mana_regen = in.mana_regen; + ooc_regen = in.ooc_regen; + maxlevel = in.maxlevel; + scalerate = in.scalerate; + invisible = in.invisible; + invisible_undead = in.invisible_undead; + invisible_animals = in.invisible_animals; + sneaking = in.sneaking; + hidden = in.hidden; + improved_hidden = in.improved_hidden; + invulnerable = in.invulnerable; + qglobal = in.qglobal; + spawned = in.spawned; + rare_spawn = in.rare_spawn; + always_aggro = in.always_aggro; + heroic_strikethrough = in.heroic_strikethrough; + keeps_sold_items = in.keeps_sold_items; + + for (int i = 0; i < MAX_APPEARANCE_EFFECTS; i++) { + appearance_effects_id[i] = in.appearance_effects_id[i]; + appearance_effects_slot[i] = in.appearance_effects_slot[i]; + } + } }; #endif diff --git a/zone/trading.cpp b/zone/trading.cpp index bf7546f1d..409211669 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -26,6 +26,8 @@ #include "../common/repositories/trader_repository.h" #include "../common/repositories/buyer_repository.h" #include "../common/repositories/buyer_buy_lines_repository.h" +#include "../common/repositories/character_offline_transactions_repository.h" +#include "../common/repositories/account_repository.h" #include "client.h" #include "entity.h" @@ -907,6 +909,7 @@ void Client::TraderStartTrader(const EQApplicationPacket *app) SetTrader(true); SendTraderMode(TraderOn); SendBecomeTraderToWorld(this, TraderOn); + UpdateWho(); LogTrading("Trader Mode ON for Player [{}] with client version {}.", GetCleanName(), (uint32) ClientVersion()); } @@ -927,6 +930,7 @@ void Client::TraderEndTrader() WithCustomer(0); SetTrader(false); + UpdateWho(); } void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Trader &t) { @@ -1402,22 +1406,6 @@ void Client::TradeRequestFailed(TraderBuy_Struct &in) QueuePacket(&outapp); } -// static void BazaarAuditTrail(const char *seller, const char *buyer, const char *itemName, int quantity, int totalCost, int tranType) { -// -// const std::string& query = fmt::format( -// "INSERT INTO `trader_audit` " -// "(`time`, `seller`, `buyer`, `itemname`, `quantity`, `totalcost`, `trantype`) " -// "VALUES (NOW(), '{}', '{}', '{}', {}, {}, {})", -// seller, -// buyer, -// Strings::Escape(itemName), -// quantity, -// totalCost, -// tranType -// ); -// database.QueryDatabase(query); -// } - void Client::BuyTraderItem(const EQApplicationPacket *app) { auto in = reinterpret_cast(app->pBuffer); @@ -1594,6 +1582,7 @@ void Client::BuyTraderItem(const EQApplicationPacket *app) .charges = buy_inst->GetCharges(), .total_cost = total_cost, .player_money_balance = GetCarriedMoney(), + .offline_purchase = trader->IsOffline(), }; RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); @@ -1616,9 +1605,22 @@ void Client::BuyTraderItem(const EQApplicationPacket *app) .charges = buy_inst->GetCharges(), .total_cost = total_cost, .player_money_balance = trader->GetCarriedMoney(), + .offline_purchase = trader->IsOffline(), }; RecordPlayerEventLogWithClient(trader, PlayerEvent::TRADER_SELL, e); + + if (trader->IsOffline()) { + auto e = CharacterOfflineTransactionsRepository::NewEntity(); + e.character_id = trader->CharacterID(); + e.item_name = buy_inst->GetItem()->Name; + e.price = total_cost; + e.quantity = quantity; + e.type = TRADER_TRANSACTION; + e.buyer_name = GetCleanName(); + + CharacterOfflineTransactionsRepository::InsertOne(database, e); + } } } @@ -1972,12 +1974,18 @@ void Client::SellToBuyer(const EQApplicationPacket *app) } if (!DoBarterBuyerChecks(sell_line)) { + SendBarterBuyerClientMessage( + sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure + ); return; - }; + } if (!DoBarterSellerChecks(sell_line)) { + SendBarterBuyerClientMessage( + sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure + ); return; - }; + } BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr)); @@ -2078,6 +2086,18 @@ void Client::SellToBuyer(const EQApplicationPacket *app) RecordPlayerEventLog(PlayerEvent::BARTER_TRANSACTION, e); } + if (buyer->IsOffline()) { + auto e = CharacterOfflineTransactionsRepository::NewEntity(); + e.character_id = buyer->CharacterID(); + e.item_name = sell_line.item_name; + e.price = total_cost; + e.quantity = sell_line.seller_quantity; + e.type = BUYER_TRANSACTION; + e.buyer_name = GetCleanName(); + + CharacterOfflineTransactionsRepository::InsertOne(database, e); + } + SendWindowUpdatesToSellerAndBuyer(sell_line); SendBarterBuyerClientMessage( sell_line, @@ -2220,6 +2240,7 @@ void Client::ToggleBuyerMode(bool status) SetCustomerID(0); SendBuyerMode(true); SendBuyerToBarterWindow(this, Barter_AddToBarterWindow); + UpdateWho(); Message(Chat::Yellow, "Barter Mode ON."); } else { @@ -2232,6 +2253,8 @@ void Client::ToggleBuyerMode(bool status) if (!IsInBuyerSpace()) { Message(Chat::Red, "You must be in a Barter Stall to start Barter Mode."); } + + UpdateWho(); Message(Chat::Yellow, fmt::format("Barter Mode OFF. Buy lines deactivated.").c_str()); } @@ -2793,8 +2816,9 @@ std::string Client::DetermineMoneyString(uint64 cp) void Client::BuyTraderItemFromBazaarWindow(const EQApplicationPacket *app) { - auto in = reinterpret_cast(app->pBuffer); - auto trader_item = TraderRepository::GetItemByItemUniqueNumber(database, in->item_unique_id); + auto in = reinterpret_cast(app->pBuffer); + auto trader_item = TraderRepository::GetItemByItemUniqueNumber(database, in->item_unique_id); + auto offline = AccountRepository::GetAllOfflineStatus(database, trader_item.char_id); LogTradingDetail( "Packet details: \n" diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 4c01e7a38..1c03b0f21 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -62,6 +62,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/skill_caps.h" #include "../common/server_reload_types.h" #include "queryserv.h" +#include "../common/repositories/account_repository.h" +#include "../common/repositories/character_offline_transactions_repository.h" extern EntityList entity_list; extern Zone *zone; @@ -3876,10 +3878,23 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) .charges = in->item_charges, .total_cost = total_cost, .player_money_balance = trader_pc->GetCarriedMoney(), + .offline_purchase = trader_pc->IsOffline(), }; RecordPlayerEventLogWithClient(trader_pc, PlayerEvent::TRADER_SELL, e); } + if (trader_pc->IsOffline()) { + auto e = CharacterOfflineTransactionsRepository::NewEntity(); + e.character_id = trader_pc->CharacterID(); + e.item_name = in->trader_buy_struct.item_name; + e.price = in->trader_buy_struct.price * in->trader_buy_struct.quantity; + e.quantity = in->trader_buy_struct.quantity; + e.type = TRADER_TRANSACTION; + e.buyer_name = in->trader_buy_struct.buyer_name; + + CharacterOfflineTransactionsRepository::InsertOne(database, e); + } + in->transaction_status = BazaarPurchaseSuccess; TraderRepository::UpdateActiveTransaction(database, in->id, false); worldserver.SendPacket(pack); @@ -4274,6 +4289,18 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) RecordPlayerEventLogWithClient(buyer, PlayerEvent::BARTER_TRANSACTION, e); } + if (buyer->IsOffline()) { + auto e = CharacterOfflineTransactionsRepository::NewEntity(); + e.character_id = buyer->CharacterID(); + e.item_name = sell_line.item_name; + e.price = (uint64) sell_line.item_cost * (uint64) in->seller_quantity; + e.quantity = sell_line.seller_quantity; + e.type = BUYER_TRANSACTION; + e.buyer_name = sell_line.seller_name; + + CharacterOfflineTransactionsRepository::InsertOne(database, e); + } + in->action = Barter_BuyerTransactionComplete; worldserver.SendPacket(pack); @@ -4334,8 +4361,103 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } + break; } + break; } + case ServerOP_UsertoWorldCancelOfflineRequest: { + auto in = reinterpret_cast(pack->pBuffer); + auto client = entity_list.GetClientByLSID(in->lsaccountid); + if (!client) { + LogLoginserverDetail("Step 6a(1) - Zone received ServerOP_UsertoWorldCancelOfflineRequest though could " + "not find client." + ); + + auto e = AccountRepository::GetWhere(database, fmt::format("`lsaccount_id` = '{}'", in->lsaccountid)); + if (!e.empty()) { + auto r = e.front(); + r.offline = 0; + AccountRepository::UpdateOne(database, r); + 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(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() + ); + AccountRepository::SetOfflineStatus(database, client->AccountID(), false); + + 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()) { + LogLoginserverDetail("Step 6b(3) - Zone ending trader mode for client {}", client->GetCleanName()); + client->TraderEndTrader(); + } + + 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); + + LogLoginserverDetail("Step 6b(7) - Zone removing client from entity_list"); + entity_list.RemoveMob(client->CastToMob()->GetID()); + + auto sp = new ServerPacket(ServerOP_UsertoWorldCancelOfflineResponse, pack->size); + auto out = reinterpret_cast(sp->pBuffer); + sp->opcode = ServerOP_UsertoWorldCancelOfflineResponse; + out->FromID = in->FromID; + out->lsaccountid = in->lsaccountid; + out->response = in->response; + out->ToID = in->ToID; + out->worldid = in->worldid; + strn0cpy(out->login, in->login, 64); + + LogLoginserverDetail("Step 6b(8) - Zone sending ServerOP_UsertoWorldCancelOfflineResponse back to world"); + worldserver.SendPacket(sp); + safe_delete(sp); + break; + } default: { LogInfo("Unknown ZS Opcode [{}] size [{}]", (int) pack->opcode, pack->size); break;