diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 3f8b29934..4358053bc 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -191,6 +191,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_containers_repository.h repositories/base/base_character_parcels_repository.h repositories/base/base_character_peqzone_flags_repository.h @@ -389,6 +390,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_containers_repository.h repositories/character_parcels_repository.h repositories/character_peqzone_flags_repository.h diff --git a/common/bazaar.cpp b/common/bazaar.cpp index 870d275a2..d2cb1c224 100644 --- a/common/bazaar.cpp +++ b/common/bazaar.cpp @@ -188,11 +188,11 @@ Bazaar::GetSearchResults( ); } else { - search_criteria_trader.append(fmt::format(" AND trader.char_id = {}", search.trader_id)); + search_criteria_trader.append(fmt::format(" AND trader.character_id = {}", search.trader_id)); } } else { - search_criteria_trader.append(fmt::format(" AND trader.char_id = {}", search.trader_id)); + search_criteria_trader.append(fmt::format(" AND trader.character_id = {}", search.trader_id)); } } @@ -263,16 +263,24 @@ Bazaar::GetSearchResults( } std::vector all_entries; - std::vector trader_items_ids{}; + std::unordered_set trader_items_ids{}; + + auto const trader_results = TraderRepository::GetBazaarTraderDetails( + db, + search_criteria_trader, + search.item_name, + field_criteria_items, + where_criteria_items, + search.max_results + ); - auto const trader_results = TraderRepository::GetBazaarTraderDetails(db, search_criteria_trader); if (trader_results.empty()) { LogTradingDetail("Bazaar - No traders found in bazaar search."); return all_entries; } for (auto const &i: trader_results) { - trader_items_ids.push_back(std::to_string(i.trader.item_id)); + trader_items_ids.emplace(std::to_string(i.trader.item_id)); } auto const item_results = ItemsRepository::GetItemsForBazaarSearch( @@ -291,43 +299,38 @@ Bazaar::GetSearchResults( all_entries.reserve(trader_results.size()); - for (auto const& t:trader_results) { - if (!item_results.contains(t.trader.item_id)) { - continue; - } + for (auto const& t:trader_results) { + if (!item_results.contains(t.trader.item_id)) { + continue; + } - BazaarSearchResultsFromDB_Struct r{}; - r.count = 1; - r.trader_id = t.trader.char_id; - r.serial_number = t.trader.item_sn; - r.cost = t.trader.item_cost; - r.slot_id = t.trader.slot_id; - r.charges = t.trader.item_charges; - r.stackable = item_results.at(t.trader.item_id).stackable; - r.icon_id = item_results.at(t.trader.item_id).icon; - r.trader_zone_id = t.trader.char_zone_id; - r.trader_zone_instance_id = t.trader.char_zone_instance_id; - r.trader_entity_id = t.trader.char_entity_id; - r.serial_number_RoF = fmt::format("{:016}\0", t.trader.item_sn); - r.item_name = fmt::format("{:.63}\0", item_results.at(t.trader.item_id).name); - r.trader_name = fmt::format("{:.63}\0", t.trader_name); - r.item_stat = item_results.at(t.trader.item_id).stats; + BazaarSearchResultsFromDB_Struct r{}; + r.count = 1; + r.trader_id = t.trader.character_id; + r.item_unique_id = t.trader.item_unique_id; + r.cost = t.trader.item_cost; + r.slot_id = t.trader.slot_id; + r.charges = t.trader.item_charges; + r.stackable = item_results.at(t.trader.item_id).stackable; + r.icon_id = item_results.at(t.trader.item_id).icon; + r.trader_zone_id = t.trader.char_zone_id; + r.trader_zone_instance_id = t.trader.char_zone_instance_id; + r.trader_entity_id = t.trader.char_entity_id; + r.item_name = fmt::format("{:.63}\0", item_results.at(t.trader.item_id).name); + r.trader_name = fmt::format("{:.63}\0", t.trader_name); + r.item_stat = item_results.at(t.trader.item_id).stats; - if (RuleB(Bazaar, UseAlternateBazaarSearch)) { - if (convert || - char_zone_id != Zones::BAZAAR || - (char_zone_id == Zones::BAZAAR && r.trader_zone_instance_id != char_zone_instance_id) - ) { - r.trader_id = TraderRepository::TRADER_CONVERT_ID + r.trader_zone_instance_id; - } - } + if (RuleB(Bazaar, UseAlternateBazaarSearch)) { + if (convert || + char_zone_id != Zones::BAZAAR || + (char_zone_id == Zones::BAZAAR && r.trader_zone_instance_id != char_zone_instance_id) + ) { + r.trader_id = TraderRepository::TRADER_CONVERT_ID + r.trader_zone_instance_id; + } + } - all_entries.push_back(r); - } - - if (all_entries.size() > search.max_results) { - all_entries.resize(search.max_results); - } + all_entries.push_back(r); + } LogTrading("Returning [{}] items from search results", all_entries.size()); diff --git a/common/database.cpp b/common/database.cpp index 7850dd896..962489890 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -33,6 +33,7 @@ #include "common/repositories/character_data_repository.h" #include "common/repositories/character_languages_repository.h" #include "common/repositories/character_leadership_abilities_repository.h" +#include "common/repositories/character_parcels_containers_repository.h" #include "common/repositories/character_parcels_repository.h" #include "common/repositories/character_pet_name_repository.h" #include "common/repositories/character_skills_repository.h" @@ -49,6 +50,8 @@ #include "common/repositories/raid_details_repository.h" #include "common/repositories/raid_members_repository.h" #include "common/repositories/reports_repository.h" +#include "common/repositories/item_unique_id_reservations_repository.h" +#include "common/repositories/offline_character_sessions_repository.h" #include "common/repositories/trader_repository.h" #include "common/repositories/variables_repository.h" #include "common/repositories/zone_repository.h" @@ -56,8 +59,10 @@ #include "common/strings.h" #include "common/zone_store.h" -#include #include +#include + +#include "common/repositories/sharedbank_repository.h" extern Client client; @@ -187,7 +192,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); @@ -199,7 +204,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( @@ -2242,6 +2251,7 @@ void Database::ClearGuildOnlineStatus() void Database::ClearTraderDetails() { TraderRepository::Truncate(*this); + AccountRepository::ClearAllOfflineStatus(*this); } void Database::ClearBuyerDetails() @@ -2265,3 +2275,531 @@ uint64_t Database::GetNextTableId(const std::string &table_name) return 1; } + +bool Database::ReserveItemUniqueId(const std::string &item_unique_id) +{ + return ItemUniqueIdReservationsRepository::Reserve(*this, item_unique_id); +} + +std::string Database::ReserveNewItemUniqueId() +{ + return ItemUniqueIdReservationsRepository::ReserveNew(*this); +} + +bool Database::EnsureItemUniqueId(std::string &item_unique_id) +{ + if (item_unique_id.empty()) { + item_unique_id = ReserveNewItemUniqueId(); + } + + if (item_unique_id.empty()) { + return false; + } + + return ReserveItemUniqueId(item_unique_id); +} + +void Database::ConvertInventoryToNewUniqueId() +{ + LogInfo("Converting inventory entries with missing item_unique_id"); + auto results = InventoryRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + + if (results.empty()) { + return; + } + + TransactionBegin(); + uint32 index = 0; + const uint32 batch_size = 1000; + std::vector queue{}; + queue.reserve(batch_size); + + for (auto &r: results) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + queue.push_back(r); + index++; + if (index >= batch_size) { + InventoryRepository::ReplaceMany(*this, queue); + index = 0; + queue.clear(); + } + } + + if (!queue.empty()) { + InventoryRepository::ReplaceMany(*this, queue); + } + + TransactionCommit(); + LogInfo("Converted {} records", results.size()); +} + +void Database::ConvertTraderToNewUniqueId() +{ + LogInfo("Converting trader entries with missing item_unique_id"); + auto results = TraderRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + + if (results.empty()) { + return; + } + + TransactionBegin(); + uint32 index = 0; + const uint32 batch_size = 1000; + std::vector queue{}; + queue.reserve(batch_size); + + for (auto &r: results) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + + queue.push_back(r); + index++; + if (index >= batch_size) { + TraderRepository::ReplaceMany(*this, queue); + index = 0; + queue.clear(); + } + } + + if (!queue.empty()) { + TraderRepository::ReplaceMany(*this, queue); + } + + TransactionCommit(); + LogInfo("Converted {} trader records", results.size()); +} + +void Database::ConvertParcelsToNewUniqueId() +{ + LogInfo("Converting parcel entries with missing item_unique_id"); + auto parcels = CharacterParcelsRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + auto parcel_contents = CharacterParcelsContainersRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + + TransactionBegin(); + + if (!parcels.empty()) { + std::vector queue{}; + queue.reserve(parcels.size()); + for (auto &r : parcels) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + + queue.push_back(r); + } + + if (!queue.empty()) { + CharacterParcelsRepository::ReplaceMany(*this, queue); + } + } + + if (!parcel_contents.empty()) { + std::vector queue{}; + queue.reserve(parcel_contents.size()); + for (auto &r : parcel_contents) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + + queue.push_back(r); + } + + if (!queue.empty()) { + CharacterParcelsContainersRepository::ReplaceMany(*this, queue); + } + } + + TransactionCommit(); + LogInfo( + "Converted {} parcel rows and {} parcel container rows", + parcels.size(), + parcel_contents.size() + ); +} + +void Database::ConvertInventorySnapshotsToNewUniqueId() +{ + LogInfo("Converting inventory snapshots with missing item_unique_id"); + auto results = InventorySnapshotsRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + + if (results.empty()) { + return; + } + + TransactionBegin(); + uint32 index = 0; + const uint32 batch_size = 1000; + std::vector queue{}; + queue.reserve(batch_size); + + for (auto &r : results) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + + queue.push_back(r); + index++; + if (index >= batch_size) { + InventorySnapshotsRepository::ReplaceMany(*this, queue); + index = 0; + queue.clear(); + } + } + + if (!queue.empty()) { + InventorySnapshotsRepository::ReplaceMany(*this, queue); + } + + TransactionCommit(); + LogInfo("Converted {} inventory snapshot rows", results.size()); +} + +void Database::ConvertSharedbankToNewUniqueId() +{ + LogInfo("Converting shared bank entries with missing item_unique_id"); + auto results = SharedbankRepository::GetWhere(*this, "`item_unique_id` IS NULL OR `item_unique_id` = ''"); + + if (results.empty()) { + return; + } + + TransactionBegin(); + uint32 index = 0; + const uint32 batch_size = 1000; + std::vector queue{}; + queue.reserve(batch_size); + + for (auto &r: results) { + if (!EnsureItemUniqueId(r.item_unique_id)) { + continue; + } + queue.push_back(r); + index++; + if (index >= batch_size) { + SharedbankRepository::ReplaceMany(*this, queue); + index = 0; + queue.clear(); + } + } + + if (!queue.empty()) { + SharedbankRepository::ReplaceMany(*this, queue); + } + + TransactionCommit(); + LogInfo("Converted {} records", results.size()); +} + +void Database::ClearOfflineTradingState() +{ + LogInfo("Clearing offline trading state"); + ClearTraderDetails(); + ClearBuyerDetails(); + AccountRepository::ClearAllOfflineStatus(*this); + OfflineCharacterSessionsRepository::Truncate(*this); +} + +static bool DoesColumnExist(Database &db, const std::string &table_name, const std::string &column_name) +{ + auto results = db.QueryDatabase( + fmt::format( + "SHOW COLUMNS FROM `{}` LIKE '{}'", + table_name, + Strings::Escape(column_name) + ) + ); + + return results.Success() && results.RowCount() == 1; +} + +static bool GetSingleCount(Database &db, const std::string &query, uint64 &count) +{ + auto results = db.QueryDatabase(query); + if (!results.Success() || results.RowCount() == 0) { + return false; + } + + auto row = results.begin(); + count = row[0] ? Strings::ToUnsignedBigInt(row[0]) : 0; + return true; +} + +static bool ValidateItemUniqueIdMigrationSchema(Database &db, bool verbose) +{ + struct ColumnRequirement { + std::string table; + std::string column; + }; + + const std::vector required_columns = { + {"inventory", "item_unique_id"}, + {"sharedbank", "item_unique_id"}, + {"trader", "item_unique_id"}, + {"character_parcels", "item_unique_id"}, + {"character_parcels_containers", "item_unique_id"}, + {"inventory_snapshots", "item_unique_id"}, + {"account", "offline"}, + }; + + const std::vector required_tables = { + "character_offline_transactions", + "offline_character_sessions", + "item_unique_id_reservations", + }; + + bool success = true; + + for (const auto &table_name : required_tables) { + if (db.DoesTableExist(table_name)) { + continue; + } + + LogError( + "Missing required table [{}] for bazaar item unique id migration. Run database updates before continuing.", + table_name + ); + success = false; + } + + for (const auto &requirement : required_columns) { + if (!db.DoesTableExist(requirement.table)) { + LogError( + "Missing required table [{}] for bazaar item unique id migration. Run database updates before continuing.", + requirement.table + ); + success = false; + continue; + } + + if (DoesColumnExist(db, requirement.table, requirement.column)) { + continue; + } + + LogError( + "Missing required column [{}].[{}] for bazaar item unique id migration. Run database updates before continuing.", + requirement.table, + requirement.column + ); + success = false; + } + + if (verbose && success) { + LogInfo("Bazaar item unique id migration schema validation passed"); + } + + return success; +} + +bool Database::PreflightItemUniqueIdMigration(bool verbose) +{ + struct CheckTarget { + std::string table; + bool requires_uniqueness; + }; + + const std::vector targets = { + {"inventory", true}, + {"sharedbank", true}, + {"trader", true}, + {"character_parcels", false}, + {"character_parcels_containers", false}, + {"inventory_snapshots", false}, + }; + + if (!ValidateItemUniqueIdMigrationSchema(*this, verbose)) { + return false; + } + + bool success = true; + for (const auto &target : targets) { + uint64 missing = 0; + uint64 duplicates = 0; + + const auto missing_query = fmt::format( + "SELECT COUNT(*) FROM {} WHERE item_unique_id IS NULL OR item_unique_id = ''", + target.table + ); + + if (!GetSingleCount(*this, missing_query, missing)) { + LogError("Failed running item unique id preflight query [{}]", missing_query); + success = false; + continue; + } + + if (target.requires_uniqueness) { + const auto duplicate_query = fmt::format( + "SELECT COUNT(*) FROM (SELECT item_unique_id FROM {} WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' " + "GROUP BY item_unique_id HAVING COUNT(*) > 1) AS duplicates", + target.table + ); + + if (!GetSingleCount(*this, duplicate_query, duplicates)) { + LogError("Failed running item unique id duplicate preflight query [{}]", duplicate_query); + success = false; + continue; + } + } + + if (verbose || missing || duplicates) { + LogInfo( + "Item unique id preflight [{}] missing [{}] duplicate_groups [{}]", + target.table, + missing, + duplicates + ); + } + + success = success && duplicates == 0; + } + + uint64 live_cross_table_duplicates = 0; + const auto cross_table_duplicate_query = + "SELECT COUNT(*) FROM (" + "SELECT item_unique_id FROM (" + "SELECT item_unique_id FROM inventory WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' " + "UNION ALL " + "SELECT item_unique_id FROM sharedbank WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' " + "UNION ALL " + "SELECT item_unique_id FROM trader WHERE item_unique_id IS NOT NULL AND item_unique_id <> ''" + ") AS live_ids GROUP BY item_unique_id HAVING COUNT(*) > 1" + ") AS duplicates"; + + if (!GetSingleCount(*this, cross_table_duplicate_query, live_cross_table_duplicates)) { + LogError("Failed running cross-table item_unique_id preflight query"); + success = false; + } + + uint64 offline_sessions = 0; + if (!GetSingleCount(*this, "SELECT COUNT(*) FROM offline_character_sessions", offline_sessions)) { + LogError("Failed counting offline_character_sessions during preflight"); + success = false; + } + + uint64 account_offline = 0; + if (!GetSingleCount(*this, "SELECT COUNT(*) FROM account WHERE offline = 1", account_offline)) { + LogError("Failed counting offline accounts during preflight"); + success = false; + } + + if (verbose || live_cross_table_duplicates || offline_sessions || account_offline) { + LogInfo( + "Item unique id preflight live_cross_table_duplicates [{}] offline_sessions [{}] account_offline [{}]", + live_cross_table_duplicates, + offline_sessions, + account_offline + ); + } + + return success && live_cross_table_duplicates == 0; +} + +bool Database::MigrateItemUniqueIdData(bool clear_trading_state, bool verbose) +{ + if (!ValidateItemUniqueIdMigrationSchema(*this, verbose)) { + return false; + } + + if (clear_trading_state) { + ClearOfflineTradingState(); + } + + ConvertInventoryToNewUniqueId(); + ConvertSharedbankToNewUniqueId(); + ConvertTraderToNewUniqueId(); + ConvertParcelsToNewUniqueId(); + ConvertInventorySnapshotsToNewUniqueId(); + + ItemUniqueIdReservationsRepository::PopulateFromTable(*this, "inventory", "item_unique_id"); + ItemUniqueIdReservationsRepository::PopulateFromTable(*this, "sharedbank", "item_unique_id"); + ItemUniqueIdReservationsRepository::PopulateFromTable(*this, "trader", "item_unique_id"); + + return VerifyItemUniqueIdMigration(verbose); +} + +bool Database::VerifyItemUniqueIdMigration(bool verbose) +{ + if (!ValidateItemUniqueIdMigrationSchema(*this, verbose)) { + return false; + } + + uint64 inventory_missing = 0; + uint64 sharedbank_missing = 0; + uint64 trader_missing = 0; + uint64 parcel_missing = 0; + uint64 parcel_container_missing = 0; + uint64 snapshot_missing = 0; + uint64 inventory_duplicates = 0; + uint64 sharedbank_duplicates = 0; + uint64 trader_duplicates = 0; + uint64 live_cross_table_duplicates = 0; + uint64 offline_sessions = 0; + uint64 account_offline = 0; + + const bool queries_succeeded = + GetSingleCount(*this, "SELECT COUNT(*) FROM inventory WHERE item_unique_id IS NULL OR item_unique_id = ''", inventory_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM sharedbank WHERE item_unique_id IS NULL OR item_unique_id = ''", sharedbank_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM trader WHERE item_unique_id IS NULL OR item_unique_id = ''", trader_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM character_parcels WHERE item_unique_id IS NULL OR item_unique_id = ''", parcel_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM character_parcels_containers WHERE item_unique_id IS NULL OR item_unique_id = ''", parcel_container_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM inventory_snapshots WHERE item_unique_id IS NULL OR item_unique_id = ''", snapshot_missing) && + GetSingleCount(*this, "SELECT COUNT(*) FROM (SELECT item_unique_id FROM inventory WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' GROUP BY item_unique_id HAVING COUNT(*) > 1) AS duplicates", inventory_duplicates) && + GetSingleCount(*this, "SELECT COUNT(*) FROM (SELECT item_unique_id FROM sharedbank WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' GROUP BY item_unique_id HAVING COUNT(*) > 1) AS duplicates", sharedbank_duplicates) && + GetSingleCount(*this, "SELECT COUNT(*) FROM (SELECT item_unique_id FROM trader WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' GROUP BY item_unique_id HAVING COUNT(*) > 1) AS duplicates", trader_duplicates) && + GetSingleCount( + *this, + "SELECT COUNT(*) FROM (" + "SELECT item_unique_id FROM (" + "SELECT item_unique_id FROM inventory WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' " + "UNION ALL " + "SELECT item_unique_id FROM sharedbank WHERE item_unique_id IS NOT NULL AND item_unique_id <> '' " + "UNION ALL " + "SELECT item_unique_id FROM trader WHERE item_unique_id IS NOT NULL AND item_unique_id <> ''" + ") AS live_ids GROUP BY item_unique_id HAVING COUNT(*) > 1" + ") AS duplicates", + live_cross_table_duplicates + ) && + GetSingleCount(*this, "SELECT COUNT(*) FROM offline_character_sessions", offline_sessions) && + GetSingleCount(*this, "SELECT COUNT(*) FROM account WHERE offline = 1", account_offline); + + if (!queries_succeeded) { + LogError("Item unique id verification failed to execute one or more validation queries"); + return false; + } + + if (verbose || inventory_missing || sharedbank_missing || trader_missing || parcel_missing || parcel_container_missing || snapshot_missing || + inventory_duplicates || sharedbank_duplicates || trader_duplicates || live_cross_table_duplicates || offline_sessions || account_offline) { + LogInfo( + "Item unique id verification inventory_missing [{}] sharedbank_missing [{}] trader_missing [{}] " + "parcel_missing [{}] parcel_container_missing [{}] snapshot_missing [{}] inventory_duplicates [{}] " + "sharedbank_duplicates [{}] trader_duplicates [{}] live_cross_table_duplicates [{}] offline_sessions [{}] account_offline [{}]", + inventory_missing, + sharedbank_missing, + trader_missing, + parcel_missing, + parcel_container_missing, + snapshot_missing, + inventory_duplicates, + sharedbank_duplicates, + trader_duplicates, + live_cross_table_duplicates, + offline_sessions, + account_offline + ); + } + + return inventory_missing == 0 && + sharedbank_missing == 0 && + trader_missing == 0 && + parcel_missing == 0 && + parcel_container_missing == 0 && + snapshot_missing == 0 && + inventory_duplicates == 0 && + sharedbank_duplicates == 0 && + trader_duplicates == 0 && + live_cross_table_duplicates == 0 && + offline_sessions == 0 && + account_offline == 0; +} diff --git a/common/database.h b/common/database.h index 580d58add..c7e4ec319 100644 --- a/common/database.h +++ b/common/database.h @@ -158,8 +158,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( @@ -264,6 +264,18 @@ public: void Decode(std::string &in); uint64_t GetNextTableId(const std::string& table_name); + bool ReserveItemUniqueId(const std::string &item_unique_id); + std::string ReserveNewItemUniqueId(); + bool EnsureItemUniqueId(std::string &item_unique_id); + bool PreflightItemUniqueIdMigration(bool verbose = false); + bool MigrateItemUniqueIdData(bool clear_trading_state = true, bool verbose = false); + bool VerifyItemUniqueIdMigration(bool verbose = false); + void ConvertInventoryToNewUniqueId(); + void ConvertTraderToNewUniqueId(); + void ConvertParcelsToNewUniqueId(); + void ConvertInventorySnapshotsToNewUniqueId(); + void ConvertSharedbankToNewUniqueId(); + void ClearOfflineTradingState(); private: Mutex Mvarcache; diff --git a/common/database/database_update_manifest.h b/common/database/database_update_manifest.h index 9897ab203..7626d4330 100644 --- a/common/database/database_update_manifest.h +++ b/common/database/database_update_manifest.h @@ -7211,6 +7211,183 @@ ALTER TABLE `character_buffs` .sql = R"( ALTER TABLE `character_pet_buffs` ADD COLUMN `suppressed` tinyint(1) unsigned NOT NULL DEFAULT 0 AFTER `instrument_mod`; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9331, + .description = "2026_03_19_inventory_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `inventory` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `inventory` + DROP COLUMN `guid`, + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `ornament_hero_model`, + ADD UNIQUE INDEX `idx_item_unique_id` (`item_unique_id`); +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9332, + .description = "2026_03_19_sharedbank_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `sharedbank` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `sharedbank` + DROP COLUMN `guid`, + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `ornament_hero_model`, + ADD UNIQUE INDEX `idx_item_unique_id` (`item_unique_id`); +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9333, + .description = "2026_03_19_character_parcels_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `character_parcels` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `character_parcels` + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `aug_slot_6`; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9334, + .description = "2026_03_19_character_parcels_containers_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `character_parcels_containers` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `character_parcels_containers` + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `item_id`; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9335, + .description = "2026_03_19_inventory_snapshots_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `inventory_snapshots` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `inventory_snapshots` + DROP PRIMARY KEY, + CHANGE COLUMN `charid` `character_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `time_index`, + CHANGE COLUMN `slotid` `slot_id` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0' AFTER `character_id`, + CHANGE COLUMN `itemid` `item_id` INT(11) UNSIGNED NULL DEFAULT '0' AFTER `slot_id`, + CHANGE COLUMN `augslot1` `augment_one` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0' AFTER `color`, + CHANGE COLUMN `augslot2` `augment_two` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_one`, + CHANGE COLUMN `augslot3` `augment_three` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_two`, + CHANGE COLUMN `augslot4` `augment_four` MEDIUMINT(7) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_three`, + CHANGE COLUMN `augslot5` `augment_five` MEDIUMINT(7) UNSIGNED NULL DEFAULT '0' AFTER `augment_four`, + CHANGE COLUMN `augslot6` `augment_six` MEDIUMINT(7) NOT NULL DEFAULT '0' AFTER `augment_five`, + CHANGE COLUMN `ornamenticon` `ornament_icon` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `custom_data`, + CHANGE COLUMN `ornamentidfile` `ornament_idfile` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `ornament_icon`, + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `ornament_hero_model`, + DROP COLUMN `guid`; + +ALTER TABLE `inventory_snapshots` + ADD PRIMARY KEY (`time_index`, `character_id`, `slot_id`) USING BTREE; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9336, + .description = "2026_03_19_trader_item_unique_id.sql", + .check = "SHOW COLUMNS FROM `trader` LIKE 'item_unique_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `trader` + CHANGE COLUMN `char_id` `character_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `id`, + ADD COLUMN `item_unique_id` VARCHAR(16) NULL DEFAULT NULL AFTER `item_id`, + CHANGE COLUMN `aug_slot_1` `augment_one` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `item_unique_id`, + CHANGE COLUMN `aug_slot_2` `augment_two` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_one`, + CHANGE COLUMN `aug_slot_3` `augment_three` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_two`, + CHANGE COLUMN `aug_slot_4` `augment_four` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_three`, + CHANGE COLUMN `aug_slot_5` `augment_five` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_four`, + CHANGE COLUMN `aug_slot_6` `augment_six` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `augment_five`, + DROP COLUMN `item_sn`, + DROP INDEX `idx_trader_item_sn`, + DROP INDEX `idx_trader_char`, + ADD INDEX `charid_slotid` (`character_id`, `slot_id`) USING BTREE, + ADD INDEX `idx_trader_char` (`character_id`, `char_zone_id`, `char_zone_instance_id`) USING BTREE, + ADD UNIQUE INDEX `idx_item_unique_id` (`item_unique_id`); +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9337, + .description = "2026_03_19_account_offline_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`; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9338, + .description = "2026_03_19_character_offline_transactions.sql", + .check = "SHOW TABLES LIKE 'character_offline_transactions'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE IF NOT EXISTS `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 + }, + ManifestEntry{ + .version = 9339, + .description = "2026_03_19_offline_character_sessions.sql", + .check = "SHOW TABLES LIKE 'offline_character_sessions'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE IF NOT EXISTS `offline_character_sessions` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `account_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `character_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `mode` VARCHAR(16) NOT NULL DEFAULT 'trader', + `zone_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `instance_id` INT(11) NOT NULL DEFAULT '0', + `entity_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `started_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `idx_account_id` (`account_id`), + KEY `idx_character_id` (`character_id`), + KEY `idx_zone_instance` (`zone_id`, `instance_id`) +) COLLATE='latin1_swedish_ci' ENGINE=InnoDB; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9340, + .description = "2026_03_19_item_unique_id_reservations.sql", + .check = "SHOW TABLES LIKE 'item_unique_id_reservations'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE IF NOT EXISTS `item_unique_id_reservations` ( + `item_unique_id` VARCHAR(16) NOT NULL, + `reserved_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`item_unique_id`) +) COLLATE='latin1_swedish_ci' ENGINE=InnoDB; )", .content_schema_update = false }, diff --git a/common/database_schema.h b/common/database_schema.h index 091e5a7ac..406f54a4e 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -58,6 +58,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"}, @@ -80,13 +81,13 @@ namespace DatabaseSchema { {"guilds", "id"}, {"instance_list_player", "id"}, {"inventory", "character_id"}, - {"inventory_snapshots", "charid"}, + {"inventory_snapshots", "character_id"}, {"keyring", "char_id"}, {"mail", "charid"}, {"player_titlesets", "char_id"}, {"quest_globals", "charid"}, {"timers", "char_id"}, - {"trader", "char_id"}, + {"trader", "character_id"}, {"zone_flags", "charID"} }; } @@ -133,6 +134,7 @@ namespace DatabaseSchema { "character_leadership_abilities", "character_material", "character_memmed_spells", + "character_offline_transactions", "character_parcels", "character_parcels_containers", "character_pet_buffs", @@ -162,8 +164,10 @@ namespace DatabaseSchema { "instance_list_player", "inventory", "inventory_snapshots", + "item_unique_id_reservations", "keyring", "mail", + "offline_character_sessions", "petitions", "player_titlesets", "quest_globals", diff --git a/common/emu_constants.h b/common/emu_constants.h index e1f9840fe..dee8b0427 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -45,6 +45,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_limits.cpp b/common/eq_limits.cpp index 600d5dc36..51b55b18d 100644 --- a/common/eq_limits.cpp +++ b/common/eq_limits.cpp @@ -48,6 +48,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers ClientUnknown::constants::EXPANSIONS_MASK, ClientUnknown::INULL, ClientUnknown::INULL, + ClientUnknown::INULL, ClientUnknown::INULL ), /*[ClientVersion::Client62] =*/ @@ -57,6 +58,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers Client62::constants::EXPANSIONS_MASK, Client62::INULL, Client62::INULL, + Client62::INULL, Client62::INULL ), /*[ClientVersion::Titanium] =*/ @@ -66,6 +68,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers Titanium::constants::EXPANSIONS_MASK, Titanium::constants::CHARACTER_CREATION_LIMIT, Titanium::constants::SAY_LINK_BODY_SIZE, + Titanium::INULL, Titanium::INULL ), /*[ClientVersion::SoF] =*/ @@ -75,6 +78,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers SoF::constants::EXPANSIONS_MASK, SoF::constants::CHARACTER_CREATION_LIMIT, SoF::constants::SAY_LINK_BODY_SIZE, + SoF::INULL, SoF::INULL ), /*[ClientVersion::SoD] =*/ @@ -84,6 +88,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers SoD::constants::EXPANSIONS_MASK, SoD::constants::CHARACTER_CREATION_LIMIT, SoD::constants::SAY_LINK_BODY_SIZE, + SoD::INULL, SoD::INULL ), /*[ClientVersion::UF] =*/ @@ -93,6 +98,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers UF::constants::EXPANSIONS_MASK, UF::constants::CHARACTER_CREATION_LIMIT, UF::constants::SAY_LINK_BODY_SIZE, + UF::INULL, UF::INULL ), /*[ClientVersion::RoF] =*/ @@ -102,6 +108,7 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers RoF::constants::EXPANSIONS_MASK, RoF::constants::CHARACTER_CREATION_LIMIT, RoF::constants::SAY_LINK_BODY_SIZE, + RoF::INULL, RoF::INULL ), /*[ClientVersion::RoF2] =*/ @@ -111,7 +118,8 @@ static const EQ::constants::LookupEntry constants_static_lookup_entries[EQ::vers RoF2::constants::EXPANSIONS_MASK, RoF2::constants::CHARACTER_CREATION_LIMIT, RoF2::constants::SAY_LINK_BODY_SIZE, - RoF2::constants::MAX_BAZAAR_TRADERS + RoF2::constants::MAX_BAZAAR_TRADERS, + RoF2::constants::MAX_BAZAAR_TRANSACTION ) }; diff --git a/common/eq_limits.h b/common/eq_limits.h index 4bed002b6..055e282d3 100644 --- a/common/eq_limits.h +++ b/common/eq_limits.h @@ -42,6 +42,7 @@ namespace EQ int16 CharacterCreationLimit; size_t SayLinkBodySize; uint32 BazaarTraderLimit; + uint64 BazaarMaxTransaction; LookupEntry(const LookupEntry *lookup_entry) { } LookupEntry( @@ -50,14 +51,16 @@ namespace EQ uint32 ExpansionsMask, int16 CharacterCreationLimit, size_t SayLinkBodySize, - uint32 BazaarTraderLimit + uint32 BazaarTraderLimit, + uint64 BazaarMaxTransaction ) : Expansion(Expansion), ExpansionBit(ExpansionBit), ExpansionsMask(ExpansionsMask), CharacterCreationLimit(CharacterCreationLimit), SayLinkBodySize(SayLinkBodySize), - BazaarTraderLimit(BazaarTraderLimit) + BazaarTraderLimit(BazaarTraderLimit), + BazaarMaxTransaction(BazaarMaxTransaction) { } }; diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 5136770a8..3e2a25274 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -327,6 +327,7 @@ union bool buyer; bool untargetable; uint32 npc_tint_id; + bool offline; }; struct PlayerState_Struct { @@ -3094,7 +3095,7 @@ struct BazaarSearchCriteria_Struct { struct BazaarInspect_Struct { uint32 action; char player_name[64]; - uint32 serial_number; + char item_unique_id[17]; uint32 item_id; uint32 trader_id; }; @@ -3730,6 +3731,36 @@ struct Trader_Struct { /*648*/ uint32 item_cost[EQ::invtype::BAZAAR_SIZE]; }; +struct TraderItems_Struct { + std::string item_unique_id; + uint32 item_id; + uint64 item_cost; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(item_unique_id), + CEREAL_NVP(item_id), + CEREAL_NVP(item_cost) + ); + } +}; + +struct TraderClientMessaging_Struct { + /*000*/ uint32 action; + /*008*/ std::vector items; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(items) + ); + } +}; + struct ClickTrader_Struct { /*000*/ uint32 action; /*004*/ uint32 unknown_004; @@ -3743,6 +3774,52 @@ struct GetItems_Struct{ int32 charges[EQ::invtype::BAZAAR_SIZE]; }; +struct Trader2_Struct { + uint32 action; + uint32 unknown_004; + uint64 items[EQ::invtype::BAZAAR_SIZE]; + uint32 item_cost[EQ::invtype::BAZAAR_SIZE]; + std::string serial_number[EQ::invtype::BAZAAR_SIZE]; +}; + +struct BazaarTraderDetails { + uint64 item_id; + std::string unique_id; + uint64 cost; + uint64 serial_number; // backwards compatibility. Not used for RoF2 as of March 2025 + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(item_id), + CEREAL_NVP(unique_id), + CEREAL_NVP(cost), + CEREAL_NVP(serial_number) + ); + } +}; + +struct ClickTraderNew_Struct { + uint32 action; + std::vector items; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(action), + CEREAL_NVP(items) + ); + } +}; + +struct GetBazaarItems_Struct { + uint64 items[EQ::invtype::BAZAAR_SIZE]; + std::string serial_number[EQ::invtype::BAZAAR_SIZE]; + uint32 charges[EQ::invtype::BAZAAR_SIZE]; +}; + struct BecomeTrader_Struct { uint32 action; uint16 zone_id; @@ -3775,7 +3852,7 @@ struct TraderBuy_Struct { /*084*/ char seller_name[64]; /*148*/ char unknown_148[32]; /*180*/ char item_name[64]; -/*244*/ char serial_number[17]; +/*244*/ char item_unique_id[17]; /*261*/ char unknown_261[3]; /*264*/ uint32 item_id; /*268*/ uint32 price; @@ -3794,12 +3871,12 @@ struct TraderItemUpdate_Struct{ }; struct TraderPriceUpdate_Struct { -/*000*/ uint32 Action; -/*004*/ uint32 SubAction; -/*008*/ int32 SerialNumber; -/*012*/ uint32 Unknown012; -/*016*/ uint32 NewPrice; -/*020*/ uint32 Unknown016; +/*000*/ uint32 action; +/*002*/ uint32 sub_action; +/*004*/ char item_unique_id[17]; +/*021*/ char unknown_021[3]; +/*024*/ uint32 unknown_024; +/*028*/ uint32 new_price; }; struct MoneyUpdate_Struct{ @@ -3814,6 +3891,7 @@ struct TraderDelItem_Struct{ uint32 trader_id; uint32 item_id; uint32 unknown_012; + char item_unique_id[17]; }; struct TraderClick_Struct{ @@ -3837,12 +3915,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 { @@ -3865,6 +3943,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. @@ -6419,7 +6498,7 @@ struct BazaarSearchResultsFromDB_Struct { uint32 count; uint32 trader_id; uint32 item_id; - uint32 serial_number; + std::string item_unique_id; uint32 charges; uint32 cost; uint32 slot_id; @@ -6431,7 +6510,6 @@ struct BazaarSearchResultsFromDB_Struct { uint32 item_stat; bool stackable; std::string item_name; - std::string serial_number_RoF; std::string trader_name; template @@ -6441,7 +6519,7 @@ struct BazaarSearchResultsFromDB_Struct { CEREAL_NVP(count), CEREAL_NVP(trader_id), CEREAL_NVP(item_id), - CEREAL_NVP(serial_number), + CEREAL_NVP(item_unique_id), CEREAL_NVP(charges), CEREAL_NVP(cost), CEREAL_NVP(slot_id), @@ -6453,7 +6531,6 @@ struct BazaarSearchResultsFromDB_Struct { CEREAL_NVP(item_stat), CEREAL_NVP(stackable), CEREAL_NVP(item_name), - CEREAL_NVP(serial_number_RoF), CEREAL_NVP(trader_name) ); } diff --git a/common/events/player_events.h b/common/events/player_events.h index a3279b10c..58786c190 100644 --- a/common/events/player_events.h +++ b/common/events/player_events.h @@ -1099,6 +1099,7 @@ namespace PlayerEvent { int32 charges; uint64 total_cost; uint64 player_money_balance; + bool offline_purchase; // cereal template @@ -1154,7 +1155,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) ); } }; @@ -1175,7 +1177,9 @@ namespace PlayerEvent { int32 charges; uint64 total_cost; uint64 player_money_balance; + bool offline_purchase; + // cereal template void serialize(Archive& ar) { @@ -1229,7 +1233,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) ); } }; @@ -1427,6 +1432,7 @@ namespace PlayerEvent { struct ParcelRetrieve { uint32 item_id; + std::string item_unique_id; uint32 augment_1_id; uint32 augment_2_id; uint32 augment_3_id; @@ -1472,6 +1478,7 @@ namespace PlayerEvent { { ar( CEREAL_NVP(item_id), + CEREAL_NVP(item_unique_id), CEREAL_NVP(augment_1_id), CEREAL_NVP(augment_2_id), CEREAL_NVP(augment_3_id), @@ -1487,6 +1494,7 @@ namespace PlayerEvent { struct ParcelSend { uint32 item_id; + std::string item_unique_id; uint32 augment_1_id; uint32 augment_2_id; uint32 augment_3_id; @@ -1536,6 +1544,7 @@ namespace PlayerEvent { { ar( CEREAL_NVP(item_id), + CEREAL_NVP(item_unique_id), CEREAL_NVP(augment_1_id), CEREAL_NVP(augment_2_id), CEREAL_NVP(augment_3_id), @@ -1554,6 +1563,7 @@ namespace PlayerEvent { struct ParcelDelete { uint32 char_id; uint32 item_id; + std::string item_unique_id; uint32 augment_1_id; uint32 augment_2_id; uint32 augment_3_id; @@ -1602,6 +1612,7 @@ namespace PlayerEvent { { ar( CEREAL_NVP(item_id), + CEREAL_NVP(item_unique_id), CEREAL_NVP(augment_1_id), CEREAL_NVP(augment_2_id), CEREAL_NVP(augment_3_id), diff --git a/common/guild_base.cpp b/common/guild_base.cpp index 13c2af3c9..cb95e7425 100644 --- a/common/guild_base.cpp +++ b/common/guild_base.cpp @@ -842,8 +842,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_` @@ -864,6 +866,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 9554c0665..85b2b7aba 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/inventory_profile.cpp b/common/inventory_profile.cpp index 4907cd1b9..1622ca0f2 100644 --- a/common/inventory_profile.cpp +++ b/common/inventory_profile.cpp @@ -249,6 +249,10 @@ int16 EQ::InventoryProfile::PutItem(int16 slot_id, const ItemInstance& inst) } int16 EQ::InventoryProfile::PushCursor(const ItemInstance &inst) { + if (inst.GetUniqueID().empty()) { + inst.CreateUniqueID(); + } + m_cursor.push(inst.Clone()); return invslot::slotCursor; } diff --git a/common/item_instance.cpp b/common/item_instance.cpp index 3c18c2cc7..f34544391 100644 --- a/common/item_instance.cpp +++ b/common/item_instance.cpp @@ -58,8 +58,8 @@ static inline int32 GetNextItemInstSerialNumber() // // class EQ::ItemInstance // -EQ::ItemInstance::ItemInstance(const ItemData* item, int16 charges) { - +EQ::ItemInstance::ItemInstance(const ItemData* item, int16 charges) +{ if (item) { m_item = new ItemData(*item); } @@ -77,8 +77,31 @@ EQ::ItemInstance::ItemInstance(const ItemData* item, int16 charges) { m_SerialNumber = GetNextItemInstSerialNumber(); } -EQ::ItemInstance::ItemInstance(SharedDatabase *db, uint32 item_id, int16 charges) { +EQ::ItemInstance::ItemInstance(const ItemData *item, const std::string &item_unique_id, int16 charges) +{ + if (item) { + m_item = new ItemData(*item); + } + m_charges = charges; + + if (m_item && m_item->IsClassCommon()) { + m_color = m_item->Color; + } + + if (m_item && IsEvolving()) { + SetTimer("evolve", RuleI(EvolvingItems, DelayUponEquipping)); + } + + m_SerialNumber = GetNextItemInstSerialNumber(); + + if (m_item && !item_unique_id.empty()) { + SetUniqueID(item_unique_id); + } +} + +EQ::ItemInstance::ItemInstance(SharedDatabase *db, uint32 item_id, int16 charges) +{ m_item = db->GetItem(item_id); if (m_item) { @@ -142,9 +165,15 @@ EQ::ItemInstance::ItemInstance(const ItemInstance& copy) m_custom_data[iter->first] = iter->second; } - m_SerialNumber = copy.m_SerialNumber; - m_custom_data = copy.m_custom_data; - m_timers = copy.m_timers; + m_SerialNumber = copy.m_SerialNumber; + m_custom_data = copy.m_custom_data; + m_timers = copy.m_timers; + + if (copy.GetUniqueID().empty()) { + LogInfo("Creating unique item ID as part of clone process for item id {}", copy.GetID()); + copy.CreateUniqueID(); + } + m_unique_id = copy.m_unique_id; m_exp = copy.m_exp; m_evolveLvl = copy.m_evolveLvl; @@ -1995,3 +2024,11 @@ void EQ::ItemInstance::SetEvolveEquipped(const bool in) const GetTimers().at("evolve").Disable(); } + +std::string EQ::ItemInstance::GenerateUniqueID() +{ + std::string unique_hash = UniqueHashGenerator::generate(); + + LogInventoryDetail("Generated an item serial number {}", unique_hash); + return unique_hash; +} diff --git a/common/item_instance.h b/common/item_instance.h index 8c96de108..228685456 100644 --- a/common/item_instance.h +++ b/common/item_instance.h @@ -23,6 +23,10 @@ #pragma once +#include +#include +#include + #include "common/bodytypes.h" #include "common/deity.h" #include "common/eq_constants.h" @@ -37,7 +41,6 @@ class ItemParse; // Parses item packets class EvolveInfo; // Stores information about an evolving item family - // Specifies usage type for item inside EQ::ItemInstance enum ItemInstTypes { @@ -78,6 +81,8 @@ namespace EQ ItemInstance(SharedDatabase *db, uint32 item_id, int16 charges = 0); + ItemInstance(const ItemData *item, const std::string &item_unique_id, int16 charges = 0); + ItemInstance(ItemInstTypes use_type); ItemInstance(const ItemInstance& copy); @@ -161,6 +166,14 @@ namespace EQ int16 GetCharges() const { return m_charges; } void SetCharges(int16 charges) { m_charges = charges; } + int16 GetQuantityFromCharges() const + { + if (GetCharges() > 0 || IsStackable() || GetItem()->MaxCharges > 0) { + return GetCharges(); + } + + return 1; + } uint32 GetPrice() const { return m_price; } void SetPrice(uint32 price) { m_price = price; } @@ -226,8 +239,11 @@ namespace EQ std::string Serialize(int16 slot_id) const { InternalSerializedItem_Struct s; s.slot_id = slot_id; s.inst = (const void*)this; std::string ser; ser.assign((char*)&s, sizeof(InternalSerializedItem_Struct)); return ser; } void Serialize(OutBuffer& ob, int16 slot_id) const { InternalSerializedItem_Struct isi; isi.slot_id = slot_id; isi.inst = (const void*)this; ob.write((const char*)&isi, sizeof(isi)); } - inline int32 GetSerialNumber() const { return m_SerialNumber; } - inline void SetSerialNumber(int32 id) { m_SerialNumber = id; } + int32 GetSerialNumber() const { return m_SerialNumber; } + void SetSerialNumber(int32 id) { m_SerialNumber = id; } + const std::string &GetUniqueID() const { return m_unique_id; } + void SetUniqueID(std::string sn) { m_unique_id = std::move(sn); } + void CreateUniqueID() const { m_unique_id = GenerateUniqueID(); } std::map& GetTimers() const { return m_timers; } void SetTimer(std::string name, uint32 time); @@ -344,28 +360,30 @@ namespace EQ std::map::const_iterator _cbegin() { return m_contents.cbegin(); } std::map::const_iterator _cend() { return m_contents.cend(); } - void _PutItem(uint8 index, ItemInstance* inst) { m_contents[index] = inst; } + void _PutItem(uint8 index, ItemInstance *inst) { m_contents[index] = inst; } + static std::string GenerateUniqueID(); - ItemInstTypes m_use_type{ItemInstNormal};// Usage type for item - const ItemData * m_item{nullptr}; // Ptr to item data - int16 m_charges{0}; // # of charges for chargeable items - uint32 m_price{0}; // Bazaar /trader price - uint32 m_color{0}; - uint32 m_merchantslot{0}; - int16 m_currentslot{0}; - bool m_attuned{false}; - int32 m_merchantcount{1};//number avaliable on the merchant, -1=unlimited - int32 m_SerialNumber{0}; // Unique identifier for this instance of an item. Needed for Bazaar. - uint32 m_exp{0}; - int8 m_evolveLvl{0}; - ItemData * m_scaledItem{nullptr}; - bool m_scaling{false}; - uint32 m_ornamenticon{0}; - uint32 m_ornamentidfile{0}; - uint32 m_new_id_file{0}; - uint32 m_ornament_hero_model{0}; - uint32 m_recast_timestamp{0}; - int m_task_delivered_count{0}; + ItemInstTypes m_use_type{ ItemInstNormal }; // Usage type for item + const ItemData *m_item{ nullptr }; // Ptr to item data + int16 m_charges{ 0 }; // # of charges for chargeable items + uint32 m_price{ 0 }; // Bazaar /trader price + uint32 m_color{ 0 }; + uint32 m_merchantslot{ 0 }; + int16 m_currentslot{ 0 }; + bool m_attuned{ false }; + int32 m_merchantcount{ 1 }; // number avaliable on the merchant, -1=unlimited + int32 m_SerialNumber{ 0 }; // Unique identifier for this instance of an item. Needed for Bazaar. + mutable std::string m_unique_id{}; // unique serial number across all zones/world TESTING March 2025 + uint32 m_exp{ 0 }; + int8 m_evolveLvl{ 0 }; + ItemData *m_scaledItem{ nullptr }; + bool m_scaling{ false }; + uint32 m_ornamenticon{ 0 }; + uint32 m_ornamentidfile{ 0 }; + uint32 m_new_id_file{ 0 }; + uint32 m_ornament_hero_model{ 0 }; + uint32 m_recast_timestamp{ 0 }; + int m_task_delivered_count{ 0 }; mutable CharacterEvolvingItemsRepository::CharacterEvolvingItems m_evolving_details{}; // Items inside of this item (augs or contents) {}; @@ -373,4 +391,48 @@ namespace EQ std::map m_custom_data {}; mutable std::map m_timers {}; }; + + class UniqueHashGenerator + { + private: + static constexpr char ALPHANUM[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + static constexpr size_t ALPHANUM_SIZE = sizeof(ALPHANUM) - 1; + + static std::mt19937_64 &GetRng() + { + thread_local std::mt19937_64 rng = []() { + std::random_device rd; + std::array entropy{}; + + for (auto &value : entropy) { + value = rd(); + } + + auto now = static_cast( + std::chrono::steady_clock::now().time_since_epoch().count() + ); + entropy[0] ^= static_cast(now); + entropy[1] ^= static_cast(now >> 32); + + std::seed_seq seed(entropy.begin(), entropy.end()); + return std::mt19937_64(seed); + }(); + + return rng; + } + + public: + static std::string generate() + { + auto &rng = GetRng(); + std::uniform_int_distribution dist(0, ALPHANUM_SIZE - 1); + + std::array result; + for (int i = 0; i < 16; ++i) { + result[i] = ALPHANUM[dist(rng)]; + } + + return std::string(result.begin(), result.end()); + } + }; } diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 9d79d5f8c..c926093cb 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -437,7 +437,6 @@ namespace RoF2 break; } default: { - LogTradingDetail("Unhandled action [{}]", sub_action); dest->FastQueuePacket(&in); } } @@ -478,7 +477,7 @@ namespace RoF2 for (auto i: results) { VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.trader_id); //trader ID - VARSTRUCT_ENCODE_STRING(bufptr, i.serial_number_RoF.c_str()); //serial + VARSTRUCT_ENCODE_STRING(bufptr, i.item_unique_id.c_str()); //serial VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.cost); //cost VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.stackable ? i.charges : i.count); //quantity VARSTRUCT_ENCODE_TYPE(uint32, bufptr, i.item_id); //ID @@ -531,7 +530,6 @@ namespace RoF2 break; } default: { - LogTrading("(RoF2) Unhandled action [{}]", action); dest->FastQueuePacket(&in, ack_req); } } @@ -620,10 +618,6 @@ namespace RoF2 break; } default: { - LogTrading( - "(RoF2) Unhandled action [{}]", - in->action - ); dest->QueuePacket(inapp); } } @@ -755,7 +749,7 @@ namespace RoF2 ar(bl); //packet size - auto packet_size = bl.item_name.length() + 1 + 34; + uint32 packet_size = bl.item_name.length() + 1 + 34; for (auto const &b: bl.trade_items) { packet_size += b.item_name.length() + 1; packet_size += 12; @@ -1843,7 +1837,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 @@ -1860,14 +1854,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(); } @@ -4113,26 +4105,41 @@ namespace RoF2 break; } case ListTraderItems: { - ENCODE_LENGTH_EXACT(Trader_Struct); - SETUP_DIRECT_ENCODE(Trader_Struct, structs::ClickTrader_Struct); LogTrading("(RoF2) action [{}]", action); - eq->action = structs::RoF2BazaarTraderBuyerActions::ListTraderItems; - std::transform( - std::begin(emu->items), - std::end(emu->items), - std::begin(eq->items), - [&](const uint32 x) { - return x; - } - ); - std::copy_n( - std::begin(emu->item_cost), - EQ::invtype::BAZAAR_SIZE, - std::begin(eq->item_cost) - ); + EQApplicationPacket *in = *p; + *p = nullptr; - FINISH_ENCODE(); + TraderClientMessaging_Struct tcm{}; + EQ::Util::MemoryStreamReader ss(reinterpret_cast(in->pBuffer), in->size); + cereal::BinaryInputArchive ar(ss); + { + ar(tcm); + } + + auto buffer = new char[4404]{}; // 4404 is the fixed size of the packet for 200 item limit of RoF2 + auto pos = buffer; + + auto pos_unique_id = buffer + 4; + auto pos_cost = buffer + 3604; + VARSTRUCT_ENCODE_TYPE(uint32, pos, structs::RoF2BazaarTraderBuyerActions::ListTraderItems); + for (auto const &t: tcm.items) { + strn0cpy(pos_unique_id, t.item_unique_id.data(), t.item_unique_id.length() + 1); + *(uint32 *) pos_cost = t.item_cost; + pos_unique_id += 18; + pos_cost += 4; + } + + for (int i = tcm.items.size(); i < EQ::invtype::BAZAAR_SIZE; i++) { + strn0cpy(pos_unique_id, "0000000000000000", 18); + pos_unique_id += 18; + } + + safe_delete_array(in->pBuffer); + in->pBuffer = reinterpret_cast(buffer); + in->size = 4404; + + dest->FastQueuePacket(&in); break; } case TraderAck2: { @@ -4145,7 +4152,7 @@ namespace RoF2 } case PriceUpdate: { SETUP_DIRECT_ENCODE(TraderPriceUpdate_Struct, structs::TraderPriceUpdate_Struct); - switch (emu->SubAction) { + switch (emu->sub_action) { case BazaarPriceChange_AddItem: { auto outapp = std::make_unique( OP_Trader, @@ -4153,7 +4160,7 @@ namespace RoF2 ); auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; - data->action = emu->Action; + data->action = emu->action; data->sub_action = BazaarPriceChange_AddItem; LogTrading( "(RoF2) PriceUpdate action [{}] AddItem subaction [{}]", @@ -4171,7 +4178,7 @@ namespace RoF2 ); auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; - data->action = emu->Action; + data->action = emu->action; data->sub_action = BazaarPriceChange_RemoveItem; LogTrading( "(RoF2) PriceUpdate action [{}] RemoveItem subaction [{}]", @@ -4189,7 +4196,7 @@ namespace RoF2 ); auto data = (structs::TraderStatus_Struct *) outapp->pBuffer; - data->action = emu->Action; + data->action = emu->action; data->sub_action = BazaarPriceChange_UpdatePrice; LogTrading( "(RoF2) PriceUpdate action [{}] UpdatePrice subaction [{}]", @@ -4214,7 +4221,7 @@ namespace RoF2 "(RoF2) BuyTraderItem action [{}] item_id [{}] item_sn [{}] buyer [{}]", action, eq->item_id, - eq->serial_number, + eq->item_unique_id, eq->buyer_name ); dest->FastQueuePacket(&in); @@ -4253,7 +4260,7 @@ namespace RoF2 OUT_str(buyer_name); OUT_str(seller_name); OUT_str(item_name); - OUT_str(serial_number); + OUT_str(item_unique_id); FINISH_ENCODE(); } @@ -4263,15 +4270,13 @@ namespace RoF2 ENCODE_LENGTH_EXACT(TraderDelItem_Struct); SETUP_DIRECT_ENCODE(TraderDelItem_Struct, structs::TraderDelItem_Struct); LogTrading( - "(RoF2) trader_id [{}] item_id [{}]", + "(RoF2) trader_id [{}] item_unique_id [{}]", emu->trader_id, - emu->item_id + emu->item_unique_id ); - eq->TraderID = emu->trader_id; - auto serial = fmt::format("{:016}\n", emu->item_id); - strn0cpy(eq->SerialNumber, serial.c_str(), sizeof(eq->SerialNumber)); - LogTrading("(RoF2) TraderID [{}], SerialNumber: [{}]", emu->trader_id, emu->item_id); + eq->trader_id = emu->trader_id; + strn0cpy(eq->item_unique_id, emu->item_unique_id, sizeof(eq->item_unique_id)); FINISH_ENCODE(); } @@ -4318,13 +4323,12 @@ namespace RoF2 OUT_str(buyer_name); OUT_str(seller_name); OUT_str(item_name); - OUT_str(serial_number); + OUT_str(item_unique_id); FINISH_ENCODE(); break; } default: { - LogTrading("(RoF2) Unhandled action [{}]", action); EQApplicationPacket *in = *p; *p = nullptr; @@ -4460,6 +4464,7 @@ namespace RoF2 *p = nullptr; char *InBuffer = (char *)in->pBuffer; + std::vector p_ids { 0x430, 0x420 }; WhoAllReturnStruct *wars = (WhoAllReturnStruct*)InBuffer; @@ -4485,8 +4490,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]; @@ -4722,6 +4728,10 @@ namespace RoF2 OtherData = OtherData | 0x01; } + if (emu->offline) { + OtherData = OtherData | 0x02; + } + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, OtherData); // float EmitterScalingRadius @@ -5092,7 +5102,6 @@ namespace RoF2 } default: { auto emu = (BuyerGeneric_Struct *) __packet->pBuffer; - LogTradingDetail("(RoF2) Pass thru OP_Barter packet action [{}]", emu->action); } } } @@ -6150,21 +6159,35 @@ namespace RoF2 switch (action) { case structs::RoF2BazaarTraderBuyerActions::BeginTraderMode: { DECODE_LENGTH_EXACT(structs::BeginTrader_Struct); - SETUP_DIRECT_DECODE(ClickTrader_Struct, structs::BeginTrader_Struct); - LogTrading("(RoF2) BeginTraderMode action [{}]", action); - emu->action = TraderOn; - std::copy_n(eq->item_cost, RoF2::invtype::BAZAAR_SIZE, emu->item_cost); - std::transform( - std::begin(eq->items), - std::end(eq->items), - std::begin(emu->serial_number), - [&](const structs::TraderItemSerial_Struct x) { - return Strings::ToUnsignedBigInt(x.serial_number,0); + unsigned char *eq_buffer = __packet->pBuffer; + auto eq = (RoF2::structs::BeginTrader_Struct *) eq_buffer; + + ClickTraderNew_Struct out{}; + out.action = TraderOn; + for (auto i = 0; i < RoF2::invtype::BAZAAR_SIZE; i++) { + if (eq->item_cost[i] == 0) { + continue; } - ); - FINISH_DIRECT_DECODE(); + BazaarTraderDetails btd{}; + btd.unique_id = eq->item_unique_ids[i].item_unique_id; + btd.cost = eq->item_cost[i]; + out.items.push_back(btd); + } + + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); + { + ar(out); + } + + __packet->size = static_cast(ss.str().length()); + __packet->pBuffer = new unsigned char[__packet->size]{}; + memcpy(__packet->pBuffer, ss.str().data(), __packet->size); + safe_delete_array(eq_buffer); + + LogTrading("(RoF2) BeginTraderMode action [{}]", action); break; } case structs::RoF2BazaarTraderBuyerActions::EndTraderMode: { @@ -6187,18 +6210,14 @@ namespace RoF2 SETUP_DIRECT_DECODE(TraderPriceUpdate_Struct, structs::TraderPriceUpdate_Struct); LogTrading("(RoF2) PriceUpdate action [{}]", action); - emu->Action = PriceUpdate; - emu->SerialNumber = Strings::ToUnsignedBigInt(eq->serial_number, 0); - if (emu->SerialNumber == 0) { - LogTrading("(RoF2) Price change with invalid serial number [{}]", eq->serial_number); - } - emu->NewPrice = eq->new_price; + emu->action = PriceUpdate; + strn0cpy(emu->item_unique_id, eq->item_unique_id, sizeof(emu->item_unique_id)); + emu->new_price = eq->new_price; FINISH_DIRECT_DECODE(); break; } default: { - LogTrading("(RoF2) Unhandled action [{}]", action); } } } @@ -6282,21 +6301,12 @@ namespace RoF2 IN(item_id); IN(trader_id); emu->action = BazaarInspect; - emu->serial_number = Strings::ToUnsignedInt(eq->serial_number, 0); - if (emu->serial_number == 0) { - LogTrading( - "(RoF2) trader_id = [{}] requested a BazaarInspect with an invalid serial number of [{}]", - eq->trader_id, - eq->serial_number - ); - FINISH_DIRECT_DECODE(); - return; - } + strn0cpy(emu->item_unique_id, eq->item_unique_id, sizeof(emu->item_unique_id)); LogTrading("(RoF2) BazaarInspect action [{}] item_id [{}] serial_number [{}]", action, eq->item_id, - eq->serial_number + eq->item_unique_id ); FINISH_DIRECT_DECODE(); break; @@ -6333,13 +6343,12 @@ namespace RoF2 IN_str(buyer_name); IN_str(seller_name); IN_str(item_name); - IN_str(serial_number); + strn0cpy(emu->item_unique_id, eq->item_unique_id, sizeof(emu->item_unique_id)); FINISH_DIRECT_DECODE(); break; } default: { - LogTrading("(RoF2) Unhandled action [{}]", action); } return; } @@ -6444,7 +6453,15 @@ namespace RoF2 RoF2::structs::ItemSerializationHeader hdr; //sprintf(hdr.unknown000, "06e0002Y1W00"); - strn0cpy(hdr.unknown000, fmt::format("{:016}\0", inst->GetSerialNumber()).c_str(),sizeof(hdr.unknown000)); + + if (inst->GetUniqueID().empty()) { + strn0cpy(hdr.unknown000, fmt::format("{:016}\0", inst->GetSerialNumber()).c_str(),sizeof(hdr.unknown000)); + } + else { + strn0cpy(hdr.unknown000, inst->GetUniqueID().c_str(),sizeof(hdr.unknown000)); + hdr.unknown000[16] = '\0'; + } + hdr.stacksize = 1; diff --git a/common/patches/rof2_limits.h b/common/patches/rof2_limits.h index fbf4fc677..37cd2241d 100644 --- a/common/patches/rof2_limits.h +++ b/common/patches/rof2_limits.h @@ -306,6 +306,7 @@ namespace RoF2 const size_t SAY_LINK_BODY_SIZE = 56; const uint32 MAX_GUILD_ID = 50000; const uint32 MAX_BAZAAR_TRADERS = 600; + const uint64 MAX_BAZAAR_TRANSACTION = 3276700000000; //3276700000000 } /*constants*/ diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 84c026d5d..19e458655 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -3317,7 +3317,7 @@ struct BazaarInspect_Struct { uint32 action; uint32 unknown_004; uint32 trader_id; - char serial_number[17]; + char item_unique_id[17]; char unknown_029[3]; uint32 item_id; uint32 unknown_036; @@ -3560,19 +3560,21 @@ struct WhoAllPlayerPart4 { }; struct TraderItemSerial_Struct { - char serial_number[17]; + char item_unique_id[17]; uint8 unknown_018; - void operator=(uint32 a) { - auto _tmp = fmt::format("{:016}", a); - strn0cpy(this->serial_number, _tmp.c_str(), sizeof(this->serial_number)); + TraderItemSerial_Struct& operator=(const char* a) { + strn0cpy(this->item_unique_id, a, sizeof(this->item_unique_id)); + unknown_018 = 0; + + return *this; } }; struct BeginTrader_Struct { /*0000*/ uint32 action; -/*0004*/ TraderItemSerial_Struct items[200]; -/*3604*/ uint32 item_cost[200]; +/*0004*/ TraderItemSerial_Struct item_unique_ids[RoF2::invtype::BAZAAR_SIZE]; +/*3604*/ uint32 item_cost[RoF2::invtype::BAZAAR_SIZE]; /*4404*/ }; @@ -3598,11 +3600,11 @@ struct BazaarWindowRemoveTrader_Struct { }; struct TraderPriceUpdate_Struct { - uint32 action; - char serial_number[17]; - char unknown_021[3]; - uint32 unknown_024; - uint32 new_price; +/*000*/ uint32 action; +/*004*/ char item_unique_id[17]; +/*021*/ char unknown_021[3]; +/*024*/ uint32 unknown_024; +/*028*/ uint32 new_price; }; struct Trader_ShowItems_Struct { @@ -3620,22 +3622,22 @@ struct TraderStatus_Struct { }; struct TraderBuy_Struct { -/*000*/ uint32 action; -/*004*/ uint32 method; -/*008*/ uint32 sub_action; -/*012*/ uint32 unknown_012; -/*016*/ uint32 trader_id; -/*020*/ char buyer_name[64]; -/*084*/ char seller_name[64]; -/*148*/ char unknown_148[32]; -/*180*/ char item_name[64]; -/*244*/ char serial_number[17]; -/*261*/ char unknown_261[3]; -/*264*/ uint32 item_id; -/*268*/ uint32 price; -/*272*/ uint32 already_sold; -/*276*/ uint32 unknown_276; -/*280*/ uint32 quantity; +/*000*/ uint32 action; +/*004*/ uint32 method; +/*008*/ uint32 sub_action; +/*012*/ uint32 unknown_012; +/*016*/ uint32 trader_id; +/*020*/ char buyer_name[64]; +/*084*/ char seller_name[64]; +/*148*/ char unknown_148[32]; +/*180*/ char item_name[64]; +/*244*/ char item_unique_id[17]; +/*261*/ char unknown_261[3]; +/*264*/ uint32 item_id; +/*268*/ uint32 price; +/*272*/ uint32 already_sold; +/*276*/ uint32 unknown_276; +/*280*/ uint32 quantity; /*284*/ }; @@ -3655,10 +3657,10 @@ struct MoneyUpdate_Struct{ }; struct TraderDelItem_Struct{ - /*000*/ uint32 Unknown000; - /*004*/ uint32 TraderID; - /*008*/ char SerialNumber[17]; - /*024*/ uint32 Unknown012; + /*000*/ uint32 unknown_000; + /*004*/ uint32 trader_id; + /*008*/ char item_unique_id[17]; + /*025*/ uint32 unknown_025; /*028*/ }; @@ -3685,22 +3687,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. @@ -3735,13 +3737,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/patches/titanium.cpp b/common/patches/titanium.cpp index 98c463921..a7af45e1b 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -226,7 +226,6 @@ namespace Titanium VARSTRUCT_ENCODE_TYPE(uint32, bufptr, row->trader_entity_id); bufptr += 4; VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->item_id); - VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->serial_number); bufptr += 4; if (row->stackable) { strn0cpy( @@ -2527,7 +2526,6 @@ namespace Titanium IN(action); memcpy(emu->player_name, eq->player_name, sizeof(emu->player_name)); - IN(serial_number); FINISH_DIRECT_DECODE(); break; diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 4f3688161..857be7bfb 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -335,7 +335,6 @@ namespace UF bufptr += 64; VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 1); VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->item_id); - VARSTRUCT_ENCODE_TYPE(int32, bufptr, row->serial_number); bufptr += 4; if (row->stackable) { strn0cpy( @@ -3615,7 +3614,6 @@ namespace UF IN(action); memcpy(emu->player_name, eq->player_name, sizeof(emu->player_name)); - IN(serial_number); FINISH_DIRECT_DECODE(); break; diff --git a/common/repositories/account_repository.h b/common/repositories/account_repository.h index 42a301ec1..c12f6b1fd 100644 --- a/common/repositories/account_repository.h +++ b/common/repositories/account_repository.h @@ -107,4 +107,43 @@ 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; + } }; diff --git a/common/repositories/base/base_account_repository.h b/common/repositories/base/base_account_repository.h index bdb613f25..a9cca1c0f 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/base/base_character_parcels_containers_repository.h b/common/repositories/base/base_character_parcels_containers_repository.h index 668fc9e30..d8339c29e 100644 --- a/common/repositories/base/base_character_parcels_containers_repository.h +++ b/common/repositories/base/base_character_parcels_containers_repository.h @@ -19,18 +19,19 @@ class BaseCharacterParcelsContainersRepository { public: struct CharacterParcelsContainers { - uint32_t id; - uint32_t parcels_id; - uint32_t slot_id; - uint32_t item_id; - uint32_t aug_slot_1; - uint32_t aug_slot_2; - uint32_t aug_slot_3; - uint32_t aug_slot_4; - uint32_t aug_slot_5; - uint32_t aug_slot_6; - uint32_t quantity; - uint32_t evolve_amount; + uint32_t id; + uint32_t parcels_id; + uint32_t slot_id; + uint32_t item_id; + std::string item_unique_id; + uint32_t aug_slot_1; + uint32_t aug_slot_2; + uint32_t aug_slot_3; + uint32_t aug_slot_4; + uint32_t aug_slot_5; + uint32_t aug_slot_6; + uint32_t quantity; + uint32_t evolve_amount; }; static std::string PrimaryKey() @@ -45,6 +46,7 @@ public: "parcels_id", "slot_id", "item_id", + "item_unique_id", "aug_slot_1", "aug_slot_2", "aug_slot_3", @@ -63,6 +65,7 @@ public: "parcels_id", "slot_id", "item_id", + "item_unique_id", "aug_slot_1", "aug_slot_2", "aug_slot_3", @@ -111,18 +114,19 @@ public: { CharacterParcelsContainers e{}; - e.id = 0; - e.parcels_id = 0; - e.slot_id = 0; - e.item_id = 0; - e.aug_slot_1 = 0; - e.aug_slot_2 = 0; - e.aug_slot_3 = 0; - e.aug_slot_4 = 0; - e.aug_slot_5 = 0; - e.aug_slot_6 = 0; - e.quantity = 0; - e.evolve_amount = 0; + e.id = 0; + e.parcels_id = 0; + e.slot_id = 0; + e.item_id = 0; + e.item_unique_id = ""; + e.aug_slot_1 = 0; + e.aug_slot_2 = 0; + e.aug_slot_3 = 0; + e.aug_slot_4 = 0; + e.aug_slot_5 = 0; + e.aug_slot_6 = 0; + e.quantity = 0; + e.evolve_amount = 0; return e; } @@ -159,18 +163,19 @@ public: if (results.RowCount() == 1) { CharacterParcelsContainers e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_1 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_2 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_3 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_4 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_5 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.aug_slot_6 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.item_unique_id = row[4] ? row[4] : ""; + e.aug_slot_1 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_2 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_3 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_4 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.aug_slot_5 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.aug_slot_6 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; return e; } @@ -207,14 +212,15 @@ public: v.push_back(columns[1] + " = " + std::to_string(e.parcels_id)); v.push_back(columns[2] + " = " + std::to_string(e.slot_id)); v.push_back(columns[3] + " = " + std::to_string(e.item_id)); - v.push_back(columns[4] + " = " + std::to_string(e.aug_slot_1)); - v.push_back(columns[5] + " = " + std::to_string(e.aug_slot_2)); - v.push_back(columns[6] + " = " + std::to_string(e.aug_slot_3)); - v.push_back(columns[7] + " = " + std::to_string(e.aug_slot_4)); - v.push_back(columns[8] + " = " + std::to_string(e.aug_slot_5)); - v.push_back(columns[9] + " = " + std::to_string(e.aug_slot_6)); - v.push_back(columns[10] + " = " + std::to_string(e.quantity)); - v.push_back(columns[11] + " = " + std::to_string(e.evolve_amount)); + v.push_back(columns[4] + " = '" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(columns[5] + " = " + std::to_string(e.aug_slot_1)); + v.push_back(columns[6] + " = " + std::to_string(e.aug_slot_2)); + v.push_back(columns[7] + " = " + std::to_string(e.aug_slot_3)); + v.push_back(columns[8] + " = " + std::to_string(e.aug_slot_4)); + v.push_back(columns[9] + " = " + std::to_string(e.aug_slot_5)); + v.push_back(columns[10] + " = " + std::to_string(e.aug_slot_6)); + v.push_back(columns[11] + " = " + std::to_string(e.quantity)); + v.push_back(columns[12] + " = " + std::to_string(e.evolve_amount)); auto results = db.QueryDatabase( fmt::format( @@ -240,6 +246,7 @@ public: v.push_back(std::to_string(e.parcels_id)); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.item_id)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.aug_slot_1)); v.push_back(std::to_string(e.aug_slot_2)); v.push_back(std::to_string(e.aug_slot_3)); @@ -281,6 +288,7 @@ public: v.push_back(std::to_string(e.parcels_id)); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.item_id)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.aug_slot_1)); v.push_back(std::to_string(e.aug_slot_2)); v.push_back(std::to_string(e.aug_slot_3)); @@ -322,18 +330,19 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { CharacterParcelsContainers e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_1 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_2 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_3 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_4 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_5 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.aug_slot_6 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.item_unique_id = row[4] ? row[4] : ""; + e.aug_slot_1 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_2 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_3 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_4 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.aug_slot_5 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.aug_slot_6 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -358,18 +367,19 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { CharacterParcelsContainers e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_1 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_2 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_3 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_4 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_5 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.aug_slot_6 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.parcels_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.item_unique_id = row[4] ? row[4] : ""; + e.aug_slot_1 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_2 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_3 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_4 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.aug_slot_5 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.aug_slot_6 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -448,6 +458,7 @@ public: v.push_back(std::to_string(e.parcels_id)); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.item_id)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.aug_slot_1)); v.push_back(std::to_string(e.aug_slot_2)); v.push_back(std::to_string(e.aug_slot_3)); @@ -482,6 +493,7 @@ public: v.push_back(std::to_string(e.parcels_id)); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.item_id)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.aug_slot_1)); v.push_back(std::to_string(e.aug_slot_2)); v.push_back(std::to_string(e.aug_slot_3)); diff --git a/common/repositories/base/base_character_parcels_repository.h b/common/repositories/base/base_character_parcels_repository.h index bc5934c7d..c2f407726 100644 --- a/common/repositories/base/base_character_parcels_repository.h +++ b/common/repositories/base/base_character_parcels_repository.h @@ -28,6 +28,7 @@ public: uint32_t aug_slot_4; uint32_t aug_slot_5; uint32_t aug_slot_6; + std::string item_unique_id; uint32_t slot_id; uint32_t quantity; uint32_t evolve_amount; @@ -53,6 +54,7 @@ public: "aug_slot_4", "aug_slot_5", "aug_slot_6", + "item_unique_id", "slot_id", "quantity", "evolve_amount", @@ -74,6 +76,7 @@ public: "aug_slot_4", "aug_slot_5", "aug_slot_6", + "item_unique_id", "slot_id", "quantity", "evolve_amount", @@ -129,6 +132,7 @@ public: e.aug_slot_4 = 0; e.aug_slot_5 = 0; e.aug_slot_6 = 0; + e.item_unique_id = ""; e.slot_id = 0; e.quantity = 0; e.evolve_amount = 0; @@ -171,21 +175,22 @@ public: if (results.RowCount() == 1) { CharacterParcels e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.slot_id = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; - e.from_name = row[12] ? row[12] : ""; - e.note = row[13] ? row[13] : ""; - e.sent_date = strtoll(row[14] ? row[14] : "-1", nullptr, 10); + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_unique_id = row[9] ? row[9] : ""; + e.slot_id = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.from_name = row[13] ? row[13] : ""; + e.note = row[14] ? row[14] : ""; + e.sent_date = strtoll(row[15] ? row[15] : "-1", nullptr, 10); return e; } @@ -227,12 +232,13 @@ public: v.push_back(columns[6] + " = " + std::to_string(e.aug_slot_4)); v.push_back(columns[7] + " = " + std::to_string(e.aug_slot_5)); v.push_back(columns[8] + " = " + std::to_string(e.aug_slot_6)); - v.push_back(columns[9] + " = " + std::to_string(e.slot_id)); - v.push_back(columns[10] + " = " + std::to_string(e.quantity)); - v.push_back(columns[11] + " = " + std::to_string(e.evolve_amount)); - v.push_back(columns[12] + " = '" + Strings::Escape(e.from_name) + "'"); - v.push_back(columns[13] + " = '" + Strings::Escape(e.note) + "'"); - v.push_back(columns[14] + " = FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + v.push_back(columns[9] + " = '" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(columns[10] + " = " + std::to_string(e.slot_id)); + v.push_back(columns[11] + " = " + std::to_string(e.quantity)); + v.push_back(columns[12] + " = " + std::to_string(e.evolve_amount)); + v.push_back(columns[13] + " = '" + Strings::Escape(e.from_name) + "'"); + v.push_back(columns[14] + " = '" + Strings::Escape(e.note) + "'"); + v.push_back(columns[15] + " = FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); auto results = db.QueryDatabase( fmt::format( @@ -263,6 +269,7 @@ public: v.push_back(std::to_string(e.aug_slot_4)); v.push_back(std::to_string(e.aug_slot_5)); v.push_back(std::to_string(e.aug_slot_6)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.quantity)); v.push_back(std::to_string(e.evolve_amount)); @@ -307,6 +314,7 @@ public: v.push_back(std::to_string(e.aug_slot_4)); v.push_back(std::to_string(e.aug_slot_5)); v.push_back(std::to_string(e.aug_slot_6)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.quantity)); v.push_back(std::to_string(e.evolve_amount)); @@ -346,21 +354,22 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { CharacterParcels e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.slot_id = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; - e.from_name = row[12] ? row[12] : ""; - e.note = row[13] ? row[13] : ""; - e.sent_date = strtoll(row[14] ? row[14] : "-1", nullptr, 10); + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_unique_id = row[9] ? row[9] : ""; + e.slot_id = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.from_name = row[13] ? row[13] : ""; + e.note = row[14] ? row[14] : ""; + e.sent_date = strtoll(row[15] ? row[15] : "-1", nullptr, 10); all_entries.push_back(e); } @@ -385,21 +394,22 @@ public: for (auto row = results.begin(); row != results.end(); ++row) { CharacterParcels e{}; - e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.slot_id = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.quantity = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.evolve_amount = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; - e.from_name = row[12] ? row[12] : ""; - e.note = row[13] ? row[13] : ""; - e.sent_date = strtoll(row[14] ? row[14] : "-1", nullptr, 10); + e.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.item_unique_id = row[9] ? row[9] : ""; + e.slot_id = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.quantity = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + e.evolve_amount = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.from_name = row[13] ? row[13] : ""; + e.note = row[14] ? row[14] : ""; + e.sent_date = strtoll(row[15] ? row[15] : "-1", nullptr, 10); all_entries.push_back(e); } @@ -483,6 +493,7 @@ public: v.push_back(std::to_string(e.aug_slot_4)); v.push_back(std::to_string(e.aug_slot_5)); v.push_back(std::to_string(e.aug_slot_6)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.quantity)); v.push_back(std::to_string(e.evolve_amount)); @@ -520,6 +531,7 @@ public: v.push_back(std::to_string(e.aug_slot_4)); v.push_back(std::to_string(e.aug_slot_5)); v.push_back(std::to_string(e.aug_slot_6)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); v.push_back(std::to_string(e.slot_id)); v.push_back(std::to_string(e.quantity)); v.push_back(std::to_string(e.evolve_amount)); diff --git a/common/repositories/base/base_inventory_repository.h b/common/repositories/base/base_inventory_repository.h index f1e325d5f..92632b881 100644 --- a/common/repositories/base/base_inventory_repository.h +++ b/common/repositories/base/base_inventory_repository.h @@ -35,7 +35,7 @@ public: uint32_t ornament_icon; uint32_t ornament_idfile; int32_t ornament_hero_model; - uint64_t guid; + std::string item_unique_id; }; static std::string PrimaryKey() @@ -62,7 +62,7 @@ public: "ornament_icon", "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -85,7 +85,7 @@ public: "ornament_icon", "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -142,7 +142,7 @@ public: e.ornament_icon = 0; e.ornament_idfile = 0; e.ornament_hero_model = 0; - e.guid = 0; + e.item_unique_id = ""; return e; } @@ -195,7 +195,7 @@ public: e.ornament_icon = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_idfile = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; e.ornament_hero_model = row[15] ? static_cast(atoi(row[15])) : 0; - e.guid = row[16] ? strtoull(row[16], nullptr, 10) : 0; + e.item_unique_id = row[16] ? row[16] : ""; return e; } @@ -245,7 +245,7 @@ public: v.push_back(columns[13] + " = " + std::to_string(e.ornament_icon)); v.push_back(columns[14] + " = " + std::to_string(e.ornament_idfile)); v.push_back(columns[15] + " = " + std::to_string(e.ornament_hero_model)); - v.push_back(columns[16] + " = " + std::to_string(e.guid)); + v.push_back(columns[16] + " = '" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -283,7 +283,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -329,7 +329,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -379,7 +379,7 @@ public: e.ornament_icon = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_idfile = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; e.ornament_hero_model = row[15] ? static_cast(atoi(row[15])) : 0; - e.guid = row[16] ? strtoull(row[16], nullptr, 10) : 0; + e.item_unique_id = row[16] ? row[16] : ""; all_entries.push_back(e); } @@ -420,7 +420,7 @@ public: e.ornament_icon = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_idfile = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; e.ornament_hero_model = row[15] ? static_cast(atoi(row[15])) : 0; - e.guid = row[16] ? strtoull(row[16], nullptr, 10) : 0; + e.item_unique_id = row[16] ? row[16] : ""; all_entries.push_back(e); } @@ -511,7 +511,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -550,7 +550,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/base/base_inventory_snapshots_repository.h b/common/repositories/base/base_inventory_snapshots_repository.h index 08b162a0c..f2c512fd1 100644 --- a/common/repositories/base/base_inventory_snapshots_repository.h +++ b/common/repositories/base/base_inventory_snapshots_repository.h @@ -20,23 +20,23 @@ class BaseInventorySnapshotsRepository { public: struct InventorySnapshots { uint32_t time_index; - uint32_t charid; - uint32_t slotid; - uint32_t itemid; + uint32_t character_id; + uint32_t slot_id; + uint32_t item_id; uint16_t charges; uint32_t color; - uint32_t augslot1; - uint32_t augslot2; - uint32_t augslot3; - uint32_t augslot4; - uint32_t augslot5; - int32_t augslot6; + uint32_t augment_one; + uint32_t augment_two; + uint32_t augment_three; + uint32_t augment_four; + uint32_t augment_five; + int32_t augment_six; uint8_t instnodrop; std::string custom_data; - uint32_t ornamenticon; - uint32_t ornamentidfile; + uint32_t ornament_icon; + uint32_t ornament_idfile; int32_t ornament_hero_model; - uint64_t guid; + std::string item_unique_id; }; static std::string PrimaryKey() @@ -48,23 +48,23 @@ public: { return { "time_index", - "charid", - "slotid", - "itemid", + "character_id", + "slot_id", + "item_id", "charges", "color", - "augslot1", - "augslot2", - "augslot3", - "augslot4", - "augslot5", - "augslot6", + "augment_one", + "augment_two", + "augment_three", + "augment_four", + "augment_five", + "augment_six", "instnodrop", "custom_data", - "ornamenticon", - "ornamentidfile", + "ornament_icon", + "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -72,23 +72,23 @@ public: { return { "time_index", - "charid", - "slotid", - "itemid", + "character_id", + "slot_id", + "item_id", "charges", "color", - "augslot1", - "augslot2", - "augslot3", - "augslot4", - "augslot5", - "augslot6", + "augment_one", + "augment_two", + "augment_three", + "augment_four", + "augment_five", + "augment_six", "instnodrop", "custom_data", - "ornamenticon", - "ornamentidfile", + "ornament_icon", + "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -130,23 +130,23 @@ public: InventorySnapshots e{}; e.time_index = 0; - e.charid = 0; - e.slotid = 0; - e.itemid = 0; + e.character_id = 0; + e.slot_id = 0; + e.item_id = 0; e.charges = 0; e.color = 0; - e.augslot1 = 0; - e.augslot2 = 0; - e.augslot3 = 0; - e.augslot4 = 0; - e.augslot5 = 0; - e.augslot6 = 0; + e.augment_one = 0; + e.augment_two = 0; + e.augment_three = 0; + e.augment_four = 0; + e.augment_five = 0; + e.augment_six = 0; e.instnodrop = 0; e.custom_data = ""; - e.ornamenticon = 0; - e.ornamentidfile = 0; + e.ornament_icon = 0; + e.ornament_idfile = 0; e.ornament_hero_model = 0; - e.guid = 0; + e.item_unique_id = ""; return e; } @@ -184,23 +184,23 @@ public: InventorySnapshots e{}; e.time_index = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.charid = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slotid = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.itemid = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; e.charges = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; e.color = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.augslot1 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.augslot2 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.augslot3 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.augslot4 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.augslot5 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.augslot6 = row[11] ? static_cast(atoi(row[11])) : 0; + e.augment_one = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_two = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_three = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_four = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.augment_five = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.augment_six = row[11] ? static_cast(atoi(row[11])) : 0; e.instnodrop = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.custom_data = row[13] ? row[13] : ""; - e.ornamenticon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; - e.ornamentidfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; + e.ornament_icon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.ornament_idfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; e.ornament_hero_model = row[16] ? static_cast(atoi(row[16])) : 0; - e.guid = row[17] ? strtoull(row[17], nullptr, 10) : 0; + e.item_unique_id = row[17] ? row[17] : ""; return e; } @@ -235,23 +235,23 @@ public: auto columns = Columns(); v.push_back(columns[0] + " = " + std::to_string(e.time_index)); - v.push_back(columns[1] + " = " + std::to_string(e.charid)); - v.push_back(columns[2] + " = " + std::to_string(e.slotid)); - v.push_back(columns[3] + " = " + std::to_string(e.itemid)); + v.push_back(columns[1] + " = " + std::to_string(e.character_id)); + v.push_back(columns[2] + " = " + std::to_string(e.slot_id)); + v.push_back(columns[3] + " = " + std::to_string(e.item_id)); v.push_back(columns[4] + " = " + std::to_string(e.charges)); v.push_back(columns[5] + " = " + std::to_string(e.color)); - v.push_back(columns[6] + " = " + std::to_string(e.augslot1)); - v.push_back(columns[7] + " = " + std::to_string(e.augslot2)); - v.push_back(columns[8] + " = " + std::to_string(e.augslot3)); - v.push_back(columns[9] + " = " + std::to_string(e.augslot4)); - v.push_back(columns[10] + " = " + std::to_string(e.augslot5)); - v.push_back(columns[11] + " = " + std::to_string(e.augslot6)); + v.push_back(columns[6] + " = " + std::to_string(e.augment_one)); + v.push_back(columns[7] + " = " + std::to_string(e.augment_two)); + v.push_back(columns[8] + " = " + std::to_string(e.augment_three)); + v.push_back(columns[9] + " = " + std::to_string(e.augment_four)); + v.push_back(columns[10] + " = " + std::to_string(e.augment_five)); + v.push_back(columns[11] + " = " + std::to_string(e.augment_six)); v.push_back(columns[12] + " = " + std::to_string(e.instnodrop)); v.push_back(columns[13] + " = '" + Strings::Escape(e.custom_data) + "'"); - v.push_back(columns[14] + " = " + std::to_string(e.ornamenticon)); - v.push_back(columns[15] + " = " + std::to_string(e.ornamentidfile)); + v.push_back(columns[14] + " = " + std::to_string(e.ornament_icon)); + v.push_back(columns[15] + " = " + std::to_string(e.ornament_idfile)); v.push_back(columns[16] + " = " + std::to_string(e.ornament_hero_model)); - v.push_back(columns[17] + " = " + std::to_string(e.guid)); + v.push_back(columns[17] + " = '" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -274,23 +274,23 @@ public: std::vector v; v.push_back(std::to_string(e.time_index)); - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.slotid)); - v.push_back(std::to_string(e.itemid)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.item_id)); v.push_back(std::to_string(e.charges)); v.push_back(std::to_string(e.color)); - v.push_back(std::to_string(e.augslot1)); - v.push_back(std::to_string(e.augslot2)); - v.push_back(std::to_string(e.augslot3)); - v.push_back(std::to_string(e.augslot4)); - v.push_back(std::to_string(e.augslot5)); - v.push_back(std::to_string(e.augslot6)); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.instnodrop)); v.push_back("'" + Strings::Escape(e.custom_data) + "'"); - v.push_back(std::to_string(e.ornamenticon)); - v.push_back(std::to_string(e.ornamentidfile)); + v.push_back(std::to_string(e.ornament_icon)); + v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -321,23 +321,23 @@ public: std::vector v; v.push_back(std::to_string(e.time_index)); - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.slotid)); - v.push_back(std::to_string(e.itemid)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.item_id)); v.push_back(std::to_string(e.charges)); v.push_back(std::to_string(e.color)); - v.push_back(std::to_string(e.augslot1)); - v.push_back(std::to_string(e.augslot2)); - v.push_back(std::to_string(e.augslot3)); - v.push_back(std::to_string(e.augslot4)); - v.push_back(std::to_string(e.augslot5)); - v.push_back(std::to_string(e.augslot6)); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.instnodrop)); v.push_back("'" + Strings::Escape(e.custom_data) + "'"); - v.push_back(std::to_string(e.ornamenticon)); - v.push_back(std::to_string(e.ornamentidfile)); + v.push_back(std::to_string(e.ornament_icon)); + v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -372,23 +372,23 @@ public: InventorySnapshots e{}; e.time_index = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.charid = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slotid = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.itemid = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; e.charges = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; e.color = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.augslot1 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.augslot2 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.augslot3 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.augslot4 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.augslot5 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.augslot6 = row[11] ? static_cast(atoi(row[11])) : 0; + e.augment_one = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_two = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_three = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_four = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.augment_five = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.augment_six = row[11] ? static_cast(atoi(row[11])) : 0; e.instnodrop = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.custom_data = row[13] ? row[13] : ""; - e.ornamenticon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; - e.ornamentidfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; + e.ornament_icon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.ornament_idfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; e.ornament_hero_model = row[16] ? static_cast(atoi(row[16])) : 0; - e.guid = row[17] ? strtoull(row[17], nullptr, 10) : 0; + e.item_unique_id = row[17] ? row[17] : ""; all_entries.push_back(e); } @@ -414,23 +414,23 @@ public: InventorySnapshots e{}; e.time_index = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; - e.charid = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; - e.slotid = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.itemid = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.slot_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.item_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; e.charges = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; e.color = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.augslot1 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.augslot2 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.augslot3 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.augslot4 = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; - e.augslot5 = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; - e.augslot6 = row[11] ? static_cast(atoi(row[11])) : 0; + e.augment_one = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_two = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_three = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_four = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.augment_five = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 0; + e.augment_six = row[11] ? static_cast(atoi(row[11])) : 0; e.instnodrop = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.custom_data = row[13] ? row[13] : ""; - e.ornamenticon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; - e.ornamentidfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; + e.ornament_icon = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.ornament_idfile = row[15] ? static_cast(strtoul(row[15], nullptr, 10)) : 0; e.ornament_hero_model = row[16] ? static_cast(atoi(row[16])) : 0; - e.guid = row[17] ? strtoull(row[17], nullptr, 10) : 0; + e.item_unique_id = row[17] ? row[17] : ""; all_entries.push_back(e); } @@ -506,23 +506,23 @@ public: std::vector v; v.push_back(std::to_string(e.time_index)); - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.slotid)); - v.push_back(std::to_string(e.itemid)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.item_id)); v.push_back(std::to_string(e.charges)); v.push_back(std::to_string(e.color)); - v.push_back(std::to_string(e.augslot1)); - v.push_back(std::to_string(e.augslot2)); - v.push_back(std::to_string(e.augslot3)); - v.push_back(std::to_string(e.augslot4)); - v.push_back(std::to_string(e.augslot5)); - v.push_back(std::to_string(e.augslot6)); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.instnodrop)); v.push_back("'" + Strings::Escape(e.custom_data) + "'"); - v.push_back(std::to_string(e.ornamenticon)); - v.push_back(std::to_string(e.ornamentidfile)); + v.push_back(std::to_string(e.ornament_icon)); + v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -546,23 +546,23 @@ public: std::vector v; v.push_back(std::to_string(e.time_index)); - v.push_back(std::to_string(e.charid)); - v.push_back(std::to_string(e.slotid)); - v.push_back(std::to_string(e.itemid)); + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.item_id)); v.push_back(std::to_string(e.charges)); v.push_back(std::to_string(e.color)); - v.push_back(std::to_string(e.augslot1)); - v.push_back(std::to_string(e.augslot2)); - v.push_back(std::to_string(e.augslot3)); - v.push_back(std::to_string(e.augslot4)); - v.push_back(std::to_string(e.augslot5)); - v.push_back(std::to_string(e.augslot6)); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.instnodrop)); v.push_back("'" + Strings::Escape(e.custom_data) + "'"); - v.push_back(std::to_string(e.ornamenticon)); - v.push_back(std::to_string(e.ornamentidfile)); + v.push_back(std::to_string(e.ornament_icon)); + v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/base/base_sharedbank_repository.h b/common/repositories/base/base_sharedbank_repository.h index 4c4894021..436ec169f 100644 --- a/common/repositories/base/base_sharedbank_repository.h +++ b/common/repositories/base/base_sharedbank_repository.h @@ -34,7 +34,7 @@ public: uint32_t ornament_icon; uint32_t ornament_idfile; int32_t ornament_hero_model; - uint64_t guid; + std::string item_unique_id; }; static std::string PrimaryKey() @@ -60,7 +60,7 @@ public: "ornament_icon", "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -82,7 +82,7 @@ public: "ornament_icon", "ornament_idfile", "ornament_hero_model", - "guid", + "item_unique_id", }; } @@ -138,7 +138,7 @@ public: e.ornament_icon = 0; e.ornament_idfile = 0; e.ornament_hero_model = 0; - e.guid = 0; + e.item_unique_id = ""; return e; } @@ -190,7 +190,7 @@ public: e.ornament_icon = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.ornament_idfile = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_hero_model = row[14] ? static_cast(atoi(row[14])) : 0; - e.guid = row[15] ? strtoull(row[15], nullptr, 10) : 0; + e.item_unique_id = row[15] ? row[15] : ""; return e; } @@ -239,7 +239,7 @@ public: v.push_back(columns[12] + " = " + std::to_string(e.ornament_icon)); v.push_back(columns[13] + " = " + std::to_string(e.ornament_idfile)); v.push_back(columns[14] + " = " + std::to_string(e.ornament_hero_model)); - v.push_back(columns[15] + " = " + std::to_string(e.guid)); + v.push_back(columns[15] + " = '" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -276,7 +276,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -321,7 +321,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -370,7 +370,7 @@ public: e.ornament_icon = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.ornament_idfile = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_hero_model = row[14] ? static_cast(atoi(row[14])) : 0; - e.guid = row[15] ? strtoull(row[15], nullptr, 10) : 0; + e.item_unique_id = row[15] ? row[15] : ""; all_entries.push_back(e); } @@ -410,7 +410,7 @@ public: e.ornament_icon = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; e.ornament_idfile = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; e.ornament_hero_model = row[14] ? static_cast(atoi(row[14])) : 0; - e.guid = row[15] ? strtoull(row[15], nullptr, 10) : 0; + e.item_unique_id = row[15] ? row[15] : ""; all_entries.push_back(e); } @@ -500,7 +500,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); auto results = db.QueryDatabase( fmt::format( @@ -538,7 +538,7 @@ public: v.push_back(std::to_string(e.ornament_icon)); v.push_back(std::to_string(e.ornament_idfile)); v.push_back(std::to_string(e.ornament_hero_model)); - v.push_back(std::to_string(e.guid)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/base/base_trader_repository.h b/common/repositories/base/base_trader_repository.h index 9199c27c9..7f0ecaa04 100644 --- a/common/repositories/base/base_trader_repository.h +++ b/common/repositories/base/base_trader_repository.h @@ -19,24 +19,24 @@ class BaseTraderRepository { public: struct Trader { - uint64_t id; - uint32_t char_id; - uint32_t item_id; - uint32_t aug_slot_1; - uint32_t aug_slot_2; - uint32_t aug_slot_3; - uint32_t aug_slot_4; - uint32_t aug_slot_5; - uint32_t aug_slot_6; - uint32_t item_sn; - int32_t item_charges; - uint32_t item_cost; - uint8_t slot_id; - uint32_t char_entity_id; - uint32_t char_zone_id; - int32_t char_zone_instance_id; - uint8_t active_transaction; - time_t listing_date; + uint64_t id; + uint32_t character_id; + uint32_t item_id; + std::string item_unique_id; + uint32_t augment_one; + uint32_t augment_two; + uint32_t augment_three; + uint32_t augment_four; + uint32_t augment_five; + uint32_t augment_six; + int32_t item_charges; + uint32_t item_cost; + uint8_t slot_id; + uint32_t char_entity_id; + uint32_t char_zone_id; + int32_t char_zone_instance_id; + uint8_t active_transaction; + time_t listing_date; }; static std::string PrimaryKey() @@ -48,15 +48,15 @@ public: { return { "id", - "char_id", + "character_id", "item_id", - "aug_slot_1", - "aug_slot_2", - "aug_slot_3", - "aug_slot_4", - "aug_slot_5", - "aug_slot_6", - "item_sn", + "item_unique_id", + "augment_one", + "augment_two", + "augment_three", + "augment_four", + "augment_five", + "augment_six", "item_charges", "item_cost", "slot_id", @@ -72,15 +72,15 @@ public: { return { "id", - "char_id", + "character_id", "item_id", - "aug_slot_1", - "aug_slot_2", - "aug_slot_3", - "aug_slot_4", - "aug_slot_5", - "aug_slot_6", - "item_sn", + "item_unique_id", + "augment_one", + "augment_two", + "augment_three", + "augment_four", + "augment_five", + "augment_six", "item_charges", "item_cost", "slot_id", @@ -130,15 +130,15 @@ public: Trader e{}; e.id = 0; - e.char_id = 0; + e.character_id = 0; e.item_id = 0; - e.aug_slot_1 = 0; - e.aug_slot_2 = 0; - e.aug_slot_3 = 0; - e.aug_slot_4 = 0; - e.aug_slot_5 = 0; - e.aug_slot_6 = 0; - e.item_sn = 0; + e.item_unique_id = ""; + e.augment_one = 0; + e.augment_two = 0; + e.augment_three = 0; + e.augment_four = 0; + e.augment_five = 0; + e.augment_six = 0; e.item_charges = 0; e.item_cost = 0; e.slot_id = 0; @@ -184,15 +184,15 @@ public: Trader e{}; e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.item_sn = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.item_unique_id = row[3] ? row[3] : ""; + e.augment_one = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.augment_two = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.augment_three = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_four = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_five = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_six = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; e.item_cost = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; @@ -234,15 +234,15 @@ public: auto columns = Columns(); - v.push_back(columns[1] + " = " + std::to_string(e.char_id)); + v.push_back(columns[1] + " = " + std::to_string(e.character_id)); v.push_back(columns[2] + " = " + std::to_string(e.item_id)); - v.push_back(columns[3] + " = " + std::to_string(e.aug_slot_1)); - v.push_back(columns[4] + " = " + std::to_string(e.aug_slot_2)); - v.push_back(columns[5] + " = " + std::to_string(e.aug_slot_3)); - v.push_back(columns[6] + " = " + std::to_string(e.aug_slot_4)); - v.push_back(columns[7] + " = " + std::to_string(e.aug_slot_5)); - v.push_back(columns[8] + " = " + std::to_string(e.aug_slot_6)); - v.push_back(columns[9] + " = " + std::to_string(e.item_sn)); + v.push_back(columns[3] + " = '" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(columns[4] + " = " + std::to_string(e.augment_one)); + v.push_back(columns[5] + " = " + std::to_string(e.augment_two)); + v.push_back(columns[6] + " = " + std::to_string(e.augment_three)); + v.push_back(columns[7] + " = " + std::to_string(e.augment_four)); + v.push_back(columns[8] + " = " + std::to_string(e.augment_five)); + v.push_back(columns[9] + " = " + std::to_string(e.augment_six)); v.push_back(columns[10] + " = " + std::to_string(e.item_charges)); v.push_back(columns[11] + " = " + std::to_string(e.item_cost)); v.push_back(columns[12] + " = " + std::to_string(e.slot_id)); @@ -273,15 +273,15 @@ public: std::vector v; v.push_back(std::to_string(e.id)); - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.aug_slot_1)); - v.push_back(std::to_string(e.aug_slot_2)); - v.push_back(std::to_string(e.aug_slot_3)); - v.push_back(std::to_string(e.aug_slot_4)); - v.push_back(std::to_string(e.aug_slot_5)); - v.push_back(std::to_string(e.aug_slot_6)); - v.push_back(std::to_string(e.item_sn)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); @@ -320,15 +320,15 @@ public: std::vector v; v.push_back(std::to_string(e.id)); - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.aug_slot_1)); - v.push_back(std::to_string(e.aug_slot_2)); - v.push_back(std::to_string(e.aug_slot_3)); - v.push_back(std::to_string(e.aug_slot_4)); - v.push_back(std::to_string(e.aug_slot_5)); - v.push_back(std::to_string(e.aug_slot_6)); - v.push_back(std::to_string(e.item_sn)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); @@ -371,15 +371,15 @@ public: Trader e{}; e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.item_sn = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.item_unique_id = row[3] ? row[3] : ""; + e.augment_one = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.augment_two = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.augment_three = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_four = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_five = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_six = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; e.item_cost = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; @@ -413,15 +413,15 @@ public: Trader e{}; e.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; - e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; e.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.item_sn = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.item_unique_id = row[3] ? row[3] : ""; + e.augment_one = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.augment_two = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.augment_three = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.augment_four = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.augment_five = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.augment_six = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; e.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; e.item_cost = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; e.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; @@ -505,15 +505,15 @@ public: std::vector v; v.push_back(std::to_string(e.id)); - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.aug_slot_1)); - v.push_back(std::to_string(e.aug_slot_2)); - v.push_back(std::to_string(e.aug_slot_3)); - v.push_back(std::to_string(e.aug_slot_4)); - v.push_back(std::to_string(e.aug_slot_5)); - v.push_back(std::to_string(e.aug_slot_6)); - v.push_back(std::to_string(e.item_sn)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); @@ -545,15 +545,15 @@ public: std::vector v; v.push_back(std::to_string(e.id)); - v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.character_id)); v.push_back(std::to_string(e.item_id)); - v.push_back(std::to_string(e.aug_slot_1)); - v.push_back(std::to_string(e.aug_slot_2)); - v.push_back(std::to_string(e.aug_slot_3)); - v.push_back(std::to_string(e.aug_slot_4)); - v.push_back(std::to_string(e.aug_slot_5)); - v.push_back(std::to_string(e.aug_slot_6)); - v.push_back(std::to_string(e.item_sn)); + v.push_back("'" + Strings::Escape(e.item_unique_id) + "'"); + v.push_back(std::to_string(e.augment_one)); + v.push_back(std::to_string(e.augment_two)); + v.push_back(std::to_string(e.augment_three)); + v.push_back(std::to_string(e.augment_four)); + v.push_back(std::to_string(e.augment_five)); + v.push_back(std::to_string(e.augment_six)); v.push_back(std::to_string(e.item_charges)); v.push_back(std::to_string(e.item_cost)); v.push_back(std::to_string(e.slot_id)); diff --git a/common/repositories/buyer_repository.h b/common/repositories/buyer_repository.h index 8888acf7f..c8a09d61a 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,4 +180,24 @@ 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; + } }; 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/inventory_snapshots_repository.h b/common/repositories/inventory_snapshots_repository.h index b0605c0d9..88671cdd7 100644 --- a/common/repositories/inventory_snapshots_repository.h +++ b/common/repositories/inventory_snapshots_repository.h @@ -1,8 +1,8 @@ #pragma once -#include "common/repositories/base/base_inventory_snapshots_repository.h" - #include "common/database.h" +#include "common/repositories/base/base_inventory_snapshots_repository.h" +#include "common/repositories/inventory_repository.h" #include "common/strings.h" class InventorySnapshotsRepository: public BaseInventorySnapshotsRepository { @@ -46,16 +46,15 @@ public: // Custom extended repository methods here static int64 CountInventorySnapshots(Database& db) { - const std::string& query = "SELECT COUNT(*) FROM (SELECT * FROM `inventory_snapshots` a GROUP BY `charid`, `time_index`) b"; + const std::string &query = + "SELECT COUNT(*) FROM (SELECT * FROM `inventory_snapshots` a GROUP BY `character_id`, `time_index`) b"; auto results = db.QueryDatabase(query); - if (!results.Success() || !results.RowCount()) { return -1; } - auto row = results.begin(); - + auto row = results.begin(); const int64 count = Strings::ToBigInt(row[0]); if (count > std::numeric_limits::max()) { @@ -68,4 +67,254 @@ public: return count; } + + static int64 CountCharacterInvSnapshots(Database& db, uint32 character_id) + { + const std::string &query = fmt::format( + "SELECT COUNT(*) FROM (SELECT * FROM `inventory_snapshots` a WHERE " + "`character_id` = {} GROUP BY `time_index`) b", + character_id + ); + + auto results = db.QueryDatabase(query); + if (!results.Success() || !results.RowCount()) { + return -1; + } + + auto &row = results.begin(); + const int64 count = Strings::ToBigInt(row[0]); + + if (count > std::numeric_limits::max()) { + return -2; + } + + if (count < 0) { + return -3; + } + + return count; + } + + static void ClearCharacterInvSnapshots(Database &db, uint32 character_id, bool from_now) + { + uint32 del_time = time(nullptr); + if (!from_now) { + del_time -= RuleI(Character, InvSnapshotHistoryD) * 86400; + } + + DeleteWhere(db, fmt::format("`character_id` = {} AND `time_index` <= {}", character_id, del_time)); + } + + static void ListCharacterInvSnapshots(Database &db, uint32 character_id, std::list> &is_list) + { + const std::string &query = fmt::format( + "SELECT `time_index`, COUNT(*) FROM `inventory_snapshots` WHERE " + "`character_id` = {} GROUP BY `time_index` ORDER BY `time_index` DESC", + character_id + ); + auto results = db.QueryDatabase(query); + + if (!results.Success()) + return; + + for (auto row: results) { + is_list.emplace_back(std::pair(Strings::ToUnsignedInt(row[0]), Strings::ToInt(row[1]))); + } + } + + static bool ValidateCharacterInvSnapshotTimestamp(Database &db, uint32 character_id, uint32 timestamp) + { + if (!character_id || !timestamp) { + return false; + } + + const std::string &query = fmt::format( + "SELECT * FROM `inventory_snapshots` WHERE `character_id` = {} " + "AND `time_index` = {} LIMIT 1", + character_id, + timestamp + ); + auto results = db.QueryDatabase(query); + + if (!results.Success() || results.RowCount() == 0) { + return false; + } + + return true; + } + + static void ParseCharacterInvSnapshot( + Database &db, + uint32 character_id, + uint32 timestamp, + std::list> &parse_list) + { + const std::string &query = fmt::format( + "SELECT `slot_id`, `item_id` FROM `inventory_snapshots` " + "WHERE `character_id` = {} AND `time_index` = {} ORDER BY `slot_id`", + character_id, + timestamp + ); + auto results = db.QueryDatabase(query); + + if (!results.Success()) { + return; + } + + for (auto row: results) { + parse_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); + } + } + + static void DivergeCharacterInvSnapshotFromInventory( + Database &db, + uint32 character_id, + uint32 timestamp, + std::list> &compare_list) + { + const std::string &query = fmt::format( + "SELECT slot_id, item_id FROM `inventory_snapshots` " + "WHERE `time_index` = {0} AND `character_id` = {1} AND `slot_id` NOT IN (" + "SELECT a.`slot_id` FROM `inventory_snapshots` a JOIN `inventory` b USING (`slot_id`, `item_id`) " + "WHERE a.`time_index` = {0} AND a.`character_id` = {1} AND b.`character_id` = {1})", + timestamp, + character_id + ); + auto results = db.QueryDatabase(query); + + if (!results.Success()) { + return; + } + + for (auto row: results) { + compare_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); + } + } + + static void DivergeCharacterInventoryFromInvSnapshot( + Database &db, uint32 character_id, uint32 timestamp, std::list> &compare_list) + { + const std::string &query = fmt::format( + "SELECT `slot_id`, `item_id` FROM `inventory` WHERE " + "`character_id` = {0} AND `slot_id` NOT IN (" + "SELECT a.`slot_id` FROM `inventory` a JOIN `inventory_snapshots` b USING (`slot_id`, `item_id`) " + "WHERE b.`time_index` = {1} AND b.`character_id` = {0} AND a.`character_id` = {0})", + character_id, + timestamp + ); + + auto results = db.QueryDatabase(query); + if (!results.Success()) { + return; + } + + for (auto row: results) { + compare_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); + } + } + + static bool SaveCharacterInvSnapshot(Database &db, uint32 character_id) + { + uint32 time_index = time(nullptr); + std::vector queue{}; + + auto inventory = InventoryRepository::GetWhere(db, fmt::format("`character_id` = {}", character_id)); + if (inventory.empty()) { + LogError("Character ID [{}] inventory is empty. Snapshot not created", character_id); + return false; + } + + for (auto const &i: inventory) { + auto s = NewEntity(); + s.character_id = i.character_id; + s.item_id = i.item_id; + s.item_unique_id = i.item_unique_id; + s.augment_one = i.augment_one; + s.augment_two = i.augment_two; + s.augment_three = i.augment_three; + s.augment_four = i.augment_four; + s.augment_five = i.augment_five; + s.augment_six = i.augment_six; + s.charges = i.charges; + s.color = i.color; + s.custom_data = i.custom_data; + s.instnodrop = i.instnodrop; + s.ornament_hero_model = i.ornament_hero_model; + s.ornament_icon = i.ornament_icon; + s.ornament_idfile = i.ornament_idfile; + s.slot_id = i.slot_id; + s.time_index = time_index; + s.item_unique_id = i.item_unique_id; + queue.push_back(s); + } + + if (queue.empty()) { + LogError("Character ID [{}] inventory is empty. Snapshot not created", character_id); + return false; + } + + if (!InsertMany(db, queue)) { + LogError("Failed to created inventory snapshot for [{}]", character_id); + return false; + } + + LogInventory("Created inventory snapshot for [{}] with ([{}]) items", character_id, queue.size()); + return true; + } + + static bool RestoreCharacterInvSnapshot(Database &db, uint32 character_id, uint32 timestamp) + { + auto snapshot = GetWhere(db, fmt::format("`character_id` = {} AND `time_index` = {}", character_id, timestamp)); + if (snapshot.empty()) { + LogError("The snapshot requested could not be found. Restore failed for character id [{}] @ [{}] failed", + character_id, + timestamp + ); + return false; + } + + std::vector queue{}; + for (auto const &i: snapshot) { + auto e = InventoryRepository::NewEntity(); + e.character_id = i.character_id; + e.item_id = i.item_id; + e.item_unique_id = i.item_unique_id; + e.augment_one = i.augment_one; + e.augment_two = i.augment_two; + e.augment_three = i.augment_three; + e.augment_four = i.augment_four; + e.augment_five = i.augment_five; + e.augment_six = i.augment_six; + e.charges = i.charges; + e.color = i.color; + e.custom_data = i.custom_data; + e.instnodrop = i.instnodrop; + e.ornament_hero_model = i.ornament_hero_model; + e.ornament_icon = i.ornament_icon; + e.ornament_idfile = i.ornament_idfile; + e.slot_id = i.slot_id; + e.item_unique_id = i.item_unique_id; + queue.push_back(e); + } + + if (queue.empty()) { + LogError("The snapshot is empty. Restore failed for character id [{}] @ [{}] failed", character_id, timestamp); + return false; + } + + InventoryRepository::DeleteWhere(db, fmt::format("`character_id` = {}", character_id)); + + if (!InventoryRepository::InsertMany(db, queue)) { + LogError("A database error occurred. Restore failed for character id [{}] @ [{}] failed", character_id, timestamp); + return false; + } + + LogInventory( + "Restore complete for character id [{}] with snapshot @ [{}] with [{}] entries", + character_id, + timestamp, + queue.size() + ); + return true; + } }; diff --git a/common/repositories/item_unique_id_reservations_repository.h b/common/repositories/item_unique_id_reservations_repository.h new file mode 100644 index 000000000..1eeea299c --- /dev/null +++ b/common/repositories/item_unique_id_reservations_repository.h @@ -0,0 +1,68 @@ +#pragma once + +#include "common/database.h" +#include "common/strings.h" +#include "common/item_instance.h" + +class ItemUniqueIdReservationsRepository { +public: + static bool Reserve(Database &db, const std::string &item_unique_id) + { + if (item_unique_id.empty()) { + return false; + } + + auto results = db.QueryDatabase( + fmt::format( + "INSERT IGNORE INTO item_unique_id_reservations (item_unique_id, reserved_at) VALUES ('{}', NOW())", + Strings::Escape(item_unique_id) + ) + ); + + return results.Success(); + } + + static std::string ReserveNew(Database &db, uint32 max_attempts = 64) + { + for (uint32 attempt = 0; attempt < max_attempts; ++attempt) { + auto candidate = EQ::UniqueHashGenerator::generate(); + auto results = db.QueryDatabase( + fmt::format( + "INSERT INTO item_unique_id_reservations (item_unique_id, reserved_at) VALUES ('{}', NOW())", + Strings::Escape(candidate) + ) + ); + + if (results.Success()) { + return candidate; + } + + if (results.ErrorNumber() != 1062) { + LogError( + "Failed reserving item_unique_id [{}] (attempt {}): ({}) {}", + candidate, + attempt + 1, + results.ErrorNumber(), + results.ErrorMessage() + ); + break; + } + } + + return {}; + } + + static bool PopulateFromTable(Database &db, const std::string &table_name, const std::string &column_name) + { + auto results = db.QueryDatabase( + fmt::format( + "INSERT IGNORE INTO item_unique_id_reservations (item_unique_id, reserved_at) " + "SELECT DISTINCT {1}, NOW() FROM {0} WHERE {1} IS NOT NULL AND {1} <> ''", + table_name, + column_name + ) + ); + + return results.Success(); + } +}; diff --git a/common/repositories/items_repository.h b/common/repositories/items_repository.h index 6e3d8875c..da6124537 100644 --- a/common/repositories/items_repository.h +++ b/common/repositories/items_repository.h @@ -47,7 +47,7 @@ public: static std::unordered_map GetItemsForBazaarSearch( Database& db, - const std::vector &search_ids, + const std::unordered_set &search_ids, const std::string &name, const std::string &field_criteria_items, const std::string &where_criteria_items, @@ -57,7 +57,7 @@ public: auto query = fmt::format( "SELECT id, name, stackable, icon, {} " "FROM items " - "WHERE `name` LIKE '%%{}%%' AND {} AND id IN({}) " + "WHERE `name` LIKE '%{}%' AND {} AND id IN({}) " "ORDER BY id ASC", field_criteria_items, Strings::Escape(name), diff --git a/common/repositories/offline_character_sessions_repository.h b/common/repositories/offline_character_sessions_repository.h new file mode 100644 index 000000000..ab54bce6f --- /dev/null +++ b/common/repositories/offline_character_sessions_repository.h @@ -0,0 +1,98 @@ +#pragma once + +#include "common/database.h" +#include "common/strings.h" + +class OfflineCharacterSessionsRepository { +public: + struct OfflineCharacterSession { + uint64_t id{0}; + uint32_t account_id{0}; + uint32_t character_id{0}; + std::string mode{}; + uint32_t zone_id{0}; + int32_t instance_id{0}; + uint32_t entity_id{0}; + time_t started_at{0}; + }; + + static OfflineCharacterSession GetByAccountId(Database &db, uint32 account_id) + { + OfflineCharacterSession session{}; + auto results = db.QueryDatabase( + fmt::format( + "SELECT id, account_id, character_id, mode, zone_id, instance_id, entity_id, UNIX_TIMESTAMP(started_at) " + "FROM offline_character_sessions WHERE account_id = {} LIMIT 1", + account_id + ) + ); + + if (!results.Success() || results.RowCount() == 0) { + return session; + } + + auto row = results.begin(); + session.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; + session.account_id = row[1] ? Strings::ToUnsignedInt(row[1]) : 0; + session.character_id = row[2] ? Strings::ToUnsignedInt(row[2]) : 0; + session.mode = row[3] ? row[3] : ""; + session.zone_id = row[4] ? Strings::ToUnsignedInt(row[4]) : 0; + session.instance_id = row[5] ? Strings::ToInt(row[5]) : 0; + session.entity_id = row[6] ? Strings::ToUnsignedInt(row[6]) : 0; + session.started_at = row[7] ? Strings::ToUnsignedBigInt(row[7]) : 0; + + return session; + } + + static bool ExistsByAccountId(Database &db, uint32 account_id) + { + return GetByAccountId(db, account_id).id != 0; + } + + static bool Upsert( + Database &db, + uint32 account_id, + uint32 character_id, + const std::string &mode, + uint32 zone_id, + int32 instance_id, + uint32 entity_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "INSERT INTO offline_character_sessions (account_id, character_id, mode, zone_id, instance_id, entity_id, started_at) " + "VALUES ({}, {}, '{}', {}, {}, {}, NOW()) " + "ON DUPLICATE KEY UPDATE character_id = VALUES(character_id), mode = VALUES(mode), zone_id = VALUES(zone_id), " + "instance_id = VALUES(instance_id), entity_id = VALUES(entity_id), started_at = VALUES(started_at)", + account_id, + character_id, + Strings::Escape(mode), + zone_id, + instance_id, + entity_id + ) + ); + + return results.Success(); + } + + static bool DeleteByAccountId(Database &db, uint32 account_id) + { + return db.QueryDatabase( + fmt::format("DELETE FROM offline_character_sessions WHERE account_id = {}", account_id) + ).Success(); + } + + static bool DeleteByCharacterId(Database &db, uint32 character_id) + { + return db.QueryDatabase( + fmt::format("DELETE FROM offline_character_sessions WHERE character_id = {}", character_id) + ).Success(); + } + + static bool Truncate(Database &db) + { + return db.QueryDatabase("TRUNCATE TABLE offline_character_sessions").Success(); + } +}; diff --git a/common/repositories/trader_repository.h b/common/repositories/trader_repository.h index 2dcc30dc7..3bcceaed5 100644 --- a/common/repositories/trader_repository.h +++ b/common/repositories/trader_repository.h @@ -30,8 +30,12 @@ public: }; struct BazaarTraderSearch_Struct { - Trader trader; + Trader trader; std::string trader_name; + std::string name; + bool stackable; + uint32 icon; + uint32 stats; }; struct WelcomeData_Struct { @@ -59,9 +63,9 @@ public: if (RuleB(Bazaar, UseAlternateBazaarSearch)) { results = db.QueryDatabase(fmt::format( - "SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name " + "SELECT DISTINCT(t.character_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name " "FROM trader AS t " - "JOIN character_data AS c ON t.char_id = c.id " + "JOIN character_data AS c ON t.character_id = c.id " "WHERE t.char_zone_instance_id = {} " "ORDER BY t.char_zone_instance_id ASC " "LIMIT {}", @@ -70,13 +74,14 @@ public: ); } else { - results = db.QueryDatabase(fmt::format( - "SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name " - "FROM trader AS t " - "JOIN character_data AS c ON t.char_id = c.id " - "ORDER BY t.char_zone_instance_id ASC " - "LIMIT {}", - max_results) + results = db.QueryDatabase( + fmt::format( + "SELECT DISTINCT(t.character_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name " + "FROM trader AS t " + "JOIN character_data AS c ON t.character_id = c.id " + "ORDER BY t.char_zone_instance_id ASC " + "LIMIT {}", + max_results) ); } @@ -102,7 +107,7 @@ public: { WelcomeData_Struct e{}; - auto results = db.QueryDatabase("SELECT COUNT(DISTINCT char_id), count(char_id) FROM trader;"); + auto results = db.QueryDatabase("SELECT COUNT(DISTINCT character_id), count(character_id) FROM trader;"); if (!results.RowCount()) { return e; @@ -114,15 +119,15 @@ public: return e; } - static int UpdateItem(Database &db, uint32 char_id, uint32 new_price, uint32 item_id, uint32 item_charges) + static int UpdateItem(Database &db, uint32 character_id, uint32 new_price, uint32 item_id, uint32 item_charges) { std::vector items{}; if (item_charges == 0) { items = GetWhere( db, fmt::format( - "char_id = '{}' AND item_id = '{}'", - char_id, + "character_id = {} AND item_id = {}", + character_id, item_id ) ); @@ -131,8 +136,8 @@ public: items = GetWhere( db, fmt::format( - "char_id = '{}' AND item_id = '{}' AND item_charges = '{}'", - char_id, + "character_id = {} AND item_id = {} AND item_charges = {}", + character_id, item_id, item_charges ) @@ -156,7 +161,7 @@ public: Trader item{}; auto query = fmt::format( - "SELECT t.char_id, t.item_id, t.serialnumber, t.charges, t.item_cost, t.slot_id, t.entity_id FROM trader AS t " + "SELECT t.character_id, t.item_id, t.item_unique.id, t.charges, t.item_cost, t.slot_id, t.entity_id FROM trader AS t " "WHERE t.entity_id = {} AND t.item_id = {} AND t.item_cost = {} " "LIMIT 1;", trader_id, @@ -169,41 +174,103 @@ public: return item; } - auto row = results.begin(); - item.char_id = Strings::ToInt(row[0]); - item.item_id = Strings::ToInt(row[1]); - item.item_sn = Strings::ToInt(row[2]); - item.item_charges = Strings::ToInt(row[3]); - item.item_cost = Strings::ToInt(row[4]); - item.slot_id = Strings::ToInt(row[5]); + auto row = results.begin(); + item.character_id = Strings::ToInt(row[0]); + item.item_id = Strings::ToInt(row[1]); + item.item_unique_id = row[2] ? row[2] : ""; + item.item_charges = Strings::ToInt(row[3]); + item.item_cost = Strings::ToInt(row[4]); + item.slot_id = Strings::ToInt(row[5]); return item; } - static int UpdateQuantity(Database &db, uint32 char_id, uint32 serial_number, int16 quantity) + static int UpdateQuantity(Database &db, const std::string &item_unique_id, int16 quantity) { const auto trader_item = GetWhere( db, - fmt::format("char_id = '{}' AND item_sn = '{}' ", char_id, serial_number) + fmt::format("`item_unique_id` = '{}' ", item_unique_id) ); if (trader_item.empty() || trader_item.size() > 1) { return 0; } - auto m = trader_item[0]; + auto m = trader_item[0]; m.item_charges = quantity; m.listing_date = time(nullptr); return UpdateOne(db, m); } - static Trader GetItemBySerialNumber(Database &db, uint32 serial_number, uint32 trader_id) + static std::vector UpdatePrice(Database &db, const std::string &item_unique_id, uint32 price) + { + std::vector all_entries{}; + auto target_listing = GetWhere( + db, + fmt::format("`item_unique_id` = '{}' LIMIT 1", item_unique_id) + ); + + if (target_listing.empty()) { + return all_entries; + } + + auto target = target_listing.front(); + + const auto query = fmt::format( + "UPDATE trader SET `item_cost` = {}, `listing_date` = FROM_UNIXTIME({}) WHERE `character_id` = {} AND " + "`item_unique_id` = '{}'", + price, + time(nullptr), + target.character_id, + item_unique_id + ); + + auto results = db.QueryDatabase(query); + if (results.RowsAffected() == 0) { + return all_entries; + } + + all_entries = GetWhere( + db, + fmt::format( + "`character_id` = {} AND `item_unique_id` = '{}'", + target.character_id, + item_unique_id + ) + ); + + return all_entries; + } + + static bool UpdateEntityId(Database &db, uint32 character_id, uint32 old_entity_id, uint32 new_entity_id) + { + if (!character_id || !old_entity_id || !new_entity_id) { + return false; + } + + auto results = GetWhere( + db, + fmt::format("`character_id` = {} AND `char_entity_id` = {}", character_id, old_entity_id) + ); + + if (results.empty()) { + return false; + } + + for (auto &entry : results) { + entry.char_entity_id = new_entity_id; + } + + return ReplaceMany(db, results); + } + + static Trader GetItemByItemUniqueNumber(Database &db, std::string &item_unique_id) { Trader e{}; const auto trader_item = GetWhere( db, - fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, serial_number) + fmt::format("`item_unique_id` = '{}' LIMIT 1", item_unique_id) ); if (trader_item.empty()) { @@ -213,13 +280,12 @@ public: return trader_item.at(0); } - static Trader GetItemBySerialNumber(Database &db, std::string serial_number, uint32 trader_id) + static Trader GetItemByItemUniqueNumber(Database &db, const char* item_unique_id) { Trader e{}; - auto sn = Strings::ToUnsignedBigInt(serial_number); const auto trader_item = GetWhere( db, - fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, sn) + fmt::format("`item_unique_id` = '{}' LIMIT 1", item_unique_id) ); if (trader_item.empty()) { @@ -257,21 +323,16 @@ public: return DeleteWhere(db, fmt::format("`id` IN({})", Strings::Implode(",", delete_ids))); } - static DistinctTraders_Struct GetTraderByInstanceAndSerialnumber( - Database &db, - uint32 instance_id, - const char *serial_number - ) + static DistinctTraders_Struct GetTraderByItemUniqueNumber(Database &db, std::string &item_unique_id) { DistinctTraders_Struct trader{}; auto query = fmt::format( - "SELECT t.id, t.char_id, c.name " + "SELECT t.id, t.character_id, c.name " "FROM trader AS t " - "JOIN character_data AS c ON c.id = t.char_id " - "WHERE t.char_zone_id = 151 AND t.char_zone_instance_id = {} AND t.item_sn = {} LIMIT 1", - instance_id, - serial_number + "JOIN character_data AS c ON c.id = t.character_id " + "WHERE t.item_unique_id = '{}' LIMIT 1", + item_unique_id ); auto results = db.QueryDatabase(query); @@ -281,7 +342,6 @@ public: } auto row = results.begin(); - std::string name = row[2]; trader.trader_id = Strings::ToUnsignedInt(row[1]); trader.trader_name = row[2] ? row[2] : ""; @@ -290,14 +350,23 @@ public: static std::vector GetBazaarTraderDetails( Database &db, - std::string &search_criteria_trader + const std::string &search_criteria_trader, + const std::string &name, + const std::string &field_criteria_items, + const std::string &where_criteria_items, + uint32 max_results ) { std::vector all_entries{}; auto query = fmt::format( - "SELECT trader.*, c.`name` FROM `trader` INNER JOIN character_data AS c ON trader.char_id = c.id " - "WHERE {} ORDER BY trader.char_id ASC", + "SELECT trader.id, trader.character_id, trader.item_id, trader.item_unique_id, trader.augment_one, " + "trader.augment_two, trader.augment_three, trader.augment_four, trader.augment_five, trader.augment_six, " + "trader.item_charges, trader.item_cost, trader.slot_id, trader.char_entity_id, trader.char_zone_id, " + "trader.char_zone_instance_id, trader.active_transaction, c.`name` FROM `trader` " + "INNER JOIN character_data AS c ON trader.character_id = c.id " + "WHERE {} " + "ORDER BY trader.character_id ASC", search_criteria_trader ); @@ -312,15 +381,15 @@ public: BazaarTraderSearch_Struct e{}; e.trader.id = row[0] ? strtoull(row[0], nullptr, 10) : 0; - e.trader.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.trader.character_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; e.trader.item_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; - e.trader.aug_slot_1 = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; - e.trader.aug_slot_2 = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; - e.trader.aug_slot_3 = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; - e.trader.aug_slot_4 = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; - e.trader.aug_slot_5 = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; - e.trader.aug_slot_6 = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; - e.trader.item_sn = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; + e.trader.item_unique_id = row[3] ? row[3] : std::string(""); + e.trader.augment_one = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.trader.augment_two = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.trader.augment_three = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.trader.augment_four = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.trader.augment_five = row[8] ? static_cast(strtoul(row[8], nullptr, 10)) : 0; + e.trader.augment_six = row[9] ? static_cast(strtoul(row[9], nullptr, 10)) : 0; e.trader.item_charges = row[10] ? static_cast(atoi(row[10])) : 0; e.trader.item_cost = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; e.trader.slot_id = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; @@ -335,4 +404,47 @@ public: return all_entries; } + + static Trader GetAccountZoneIdAndInstanceIdByAccountId(Database &db, uint32 account_id) + { + auto trader_query = fmt::format( + "SELECT t.id, t.character_id, t.char_zone_id, t.char_zone_instance_id " + "FROM trader AS t " + "WHERE t.character_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.character_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; + } }; diff --git a/common/servertalk.h b/common/servertalk.h index b41e998a5..dd6d453c4 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,14 +362,24 @@ 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 { + BazaarPurchaseFailed = 0, + BazaarPurchaseSuccess = 1, + BazaarPurchaseBuyerCompleteSendToSeller = 2, + BazaarPurchaseSellerCompleteSendToBuyer = 3, + BazaarPurchaseBuyerFailed = 4, + BazaarPurchaseBuyerSuccess = 5, + BazaarPurchaseTraderFailed = 6 +}; /************ PACKET RELATED STRUCT ************/ class ServerPacket { @@ -559,6 +571,9 @@ struct ServerClientList_Struct { uint8 LFGToLevel; bool LFGMatchFilter; char LFGComments[64]; + bool trader; + bool buyer; + bool offline; }; struct ServerClientListKeepAlive_Struct { @@ -1022,6 +1037,7 @@ struct ServerGuildMemberUpdate_Struct { char member_name[64]; uint32 zone_id; uint32 last_seen; + uint32 offline_mode; }; struct ServerGuildPermissionUpdate_Struct { @@ -1776,8 +1792,15 @@ struct BazaarPurchaseMessaging_Struct { uint32 item_aug_5; uint32 item_aug_6; uint32 buyer_id; - uint32 item_quantity_available; + uint32 item_quantity; + int16 item_charges; uint32 id; + uint32 trader_zone_id; + uint32 trader_zone_instance_id; + uint32 buyer_zone_id; + uint32 buyer_zone_instance_id; + uint32 transaction_status; + bool offline_purchase; }; #pragma pack(pop) diff --git a/common/shareddb.cpp b/common/shareddb.cpp index 36746e5ad..98f02fada 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -275,7 +275,12 @@ bool SharedDatabase::UpdateInventorySlot(uint32 char_id, const EQ::ItemInstance* e.ornament_icon = inst->GetOrnamentationIcon(); e.ornament_idfile = inst->GetOrnamentationIDFile(); e.ornament_hero_model = inst->GetOrnamentHeroModel(); - e.guid = inst->GetSerialNumber(); + e.item_unique_id = inst->GetUniqueID(); + if (!EnsureItemUniqueId(e.item_unique_id)) { + return false; + } + + const_cast(inst)->SetUniqueID(e.item_unique_id); const int replaced = InventoryRepository::ReplaceOne(*this, e); @@ -325,7 +330,12 @@ bool SharedDatabase::UpdateSharedBankSlot(uint32 char_id, const EQ::ItemInstance e.ornament_icon = inst->GetOrnamentationIcon(); e.ornament_idfile = inst->GetOrnamentationIDFile(); e.ornament_hero_model = inst->GetOrnamentHeroModel(); - e.guid = inst->GetSerialNumber(); + e.item_unique_id = inst->GetUniqueID(); + if (!EnsureItemUniqueId(e.item_unique_id)) { + return false; + } + + const_cast(inst)->SetUniqueID(e.item_unique_id); const int replaced = SharedbankRepository::ReplaceOne(*this, e); @@ -630,12 +640,6 @@ bool SharedDatabase::GetInventory(Client *c) return false; } - for (auto const& row: results) { - if (row.guid != 0) { - EQ::ItemInstance::AddGUIDToMap(row.guid); - } - } - const auto timestamps = GetItemRecastTimestamps(char_id); auto cv_conflict = false; const auto pmask = inv.GetLookup()->PossessionsBitmask; @@ -713,6 +717,21 @@ bool SharedDatabase::GetInventory(Client *c) inst->SetOrnamentationIDFile(ornament_idfile); inst->SetOrnamentHeroModel(item->HerosForgeModel); + //Mass conversion handled by world + //This remains as a backup. Should not be required. + if (row.item_unique_id.empty()) { + if (!EnsureItemUniqueId(row.item_unique_id)) { + continue; + } + + inst->SetUniqueID(row.item_unique_id); + queue.push_back(row); + } + else { + ReserveItemUniqueId(row.item_unique_id); + inst->SetUniqueID(row.item_unique_id); + } + if ( instnodrop || ( @@ -727,7 +746,7 @@ bool SharedDatabase::GetInventory(Client *c) inst->SetColor(color); } - if (charges == std::numeric_limits::max()) { + if (charges > std::numeric_limits::max()) { inst->SetCharges(-1); } else if (charges == 0 && inst->IsStackable()) { // Stackable items need a minimum charge of 1 remain moveable. @@ -808,8 +827,7 @@ bool SharedDatabase::GetInventory(Client *c) put_slot_id = inv.PutItem(slot_id, *inst); } - row.guid = inst->GetSerialNumber(); - queue.push_back(row); + //queue.push_back(row); safe_delete(inst); @@ -839,8 +857,6 @@ bool SharedDatabase::GetInventory(Client *c) InventoryRepository::ReplaceMany(*this, queue); } - EQ::ItemInstance::ClearGUIDMap(); - // Retrieve shared inventory return GetSharedBank(char_id, &inv, true); } @@ -1401,7 +1417,7 @@ EQ::ItemInstance* SharedDatabase::CreateItem( return inst; } -EQ::ItemInstance* SharedDatabase::CreateBaseItem(const EQ::ItemData* item, int16 charges) { +EQ::ItemInstance* SharedDatabase::CreateBaseItem(const EQ::ItemData* item, int16 charges, const std::string &item_unique_id) { EQ::ItemInstance* inst = nullptr; if (item) { // if maxcharges is -1 that means it is an unlimited use item. @@ -1415,7 +1431,7 @@ EQ::ItemInstance* SharedDatabase::CreateBaseItem(const EQ::ItemData* item, int16 charges = 1; } - inst = new EQ::ItemInstance(item, charges); + inst = new EQ::ItemInstance(item, item_unique_id, charges); if (!inst) { LogError("Error: valid item data returned a null reference for EQ::ItemInstance creation in SharedDatabase::CreateBaseItem()"); diff --git a/common/shareddb.h b/common/shareddb.h index 559570fc9..a6a56dd43 100644 --- a/common/shareddb.h +++ b/common/shareddb.h @@ -154,7 +154,7 @@ public: uint32 ornamentidfile = 0, uint32 ornament_hero_model = 0 ); - EQ::ItemInstance *CreateBaseItem(const EQ::ItemData *item, int16 charges = 0); + EQ::ItemInstance *CreateBaseItem(const EQ::ItemData *item, int16 charges = 0, const std::string &item_unique_id = ""); void GetItemsCount(int32& item_count, uint32& max_id); void LoadItems(void *data, uint32 size, int32 items, uint32 max_item_id); diff --git a/common/strings.cpp b/common/strings.cpp index 024d8e441..29bef8993 100644 --- a/common/strings.cpp +++ b/common/strings.cpp @@ -960,3 +960,22 @@ bool Strings::IsValidJson(const std::string &json) return result; } + +std::string Strings::Implode(const std::string& glue, std::unordered_set src) +{ + if (src.empty()) { + return {}; + } + + std::ostringstream output; + std::unordered_set::iterator src_iter; + + for (src_iter = src.begin(); src_iter != src.end(); src_iter++) { + output << *src_iter << glue; + } + + std::string final_output = output.str(); + final_output.resize(output.str().size() - glue.size()); + + return final_output; +} diff --git a/common/strings.h b/common/strings.h index e00c11ab0..ab2e71011 100644 --- a/common/strings.h +++ b/common/strings.h @@ -45,6 +45,7 @@ #include #include #include +#include class Strings { public: @@ -76,6 +77,7 @@ public: static std::string Escape(const std::string &s); static std::string GetBetween(const std::string &s, std::string start_delim, std::string stop_delim); static std::string Implode(const std::string& glue, std::vector src); + static std::string Implode(const std::string& glue, std::unordered_set src); static std::string Join(const std::vector &ar, const std::string &delim); static std::string Join(const std::vector &ar, const std::string &delim); static std::string MillisecondsToTime(int duration); diff --git a/common/version.h b/common/version.h index a611351d5..22b332209 100644 --- a/common/version.h +++ b/common/version.h @@ -41,6 +41,6 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9330 +#define CURRENT_BINARY_DATABASE_VERSION 9340 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #define CUSTOM_BINARY_DATABASE_VERSION 0 diff --git a/docs/bazaar_item_unique_id_rollout.md b/docs/bazaar_item_unique_id_rollout.md new file mode 100644 index 000000000..3b9697dc2 --- /dev/null +++ b/docs/bazaar_item_unique_id_rollout.md @@ -0,0 +1,109 @@ +# Bazaar Item Unique ID And Offline Trading Rollout + +## Purpose + +This rollout converts persisted item identity and offline trader or buyer session state to the new production-safe model. The migration is designed for a maintenance window and explicitly clears any in-flight trader, buyer, and offline sessions during cutover. + +Do not reopen the server until every verification step passes. + +## Preconditions + +- Schedule a maintenance window. +- Stop new logins before beginning the migration. +- Ensure the `world` binary you are deploying includes this branch. +- Ensure operators have credentials to run schema updates and database dump commands. + +## Mandatory Backup + +Take a backup before any schema or migration command: + +```powershell +world database:dump --player-tables --login-tables --dump-path=backups --compress +``` + +If you use a separate database backup process, complete it before continuing. + +## Local Dev Validation + +Run these commands against the local dev database before production: + +```powershell +world database:updates --skip-backup +world database:item-unique-ids --preflight --verbose +world database:item-unique-ids --migrate --verbose +world database:item-unique-ids --verify --verbose +``` + +Validate these gameplay scenarios after the migration: + +- Two traders listing the same item at different prices. +- One trader changing a price without affecting another trader. +- Offline trader purchase. +- Offline buyer purchase. +- Parcel retrieval for rows that previously had missing `item_unique_id` values. +- Alternate bazaar shard search. + +## Production Sequence + +1. Bring the server into maintenance mode and stop new gameplay activity. +2. Take the mandatory backup. +3. Apply schema updates: + +```powershell +world database:updates +``` + +4. Run the migration preflight: + +```powershell +world database:item-unique-ids --preflight --verbose +``` + +5. If preflight reports missing schema, duplicate live item IDs, or other unexpected errors, stop and resolve them before continuing. +6. Run the migration: + +```powershell +world database:item-unique-ids --migrate --verbose +``` + +This step clears active trader, buyer, and offline session state. Players must re-enter those modes after deploy. + +7. Run final verification: + +```powershell +world database:item-unique-ids --verify --verbose +``` + +8. Reopen the server only after verification passes. + +## What Preflight And Verify Must Show + +The migration is not complete unless all of the following are true: + +- `inventory`, `sharedbank`, and `trader` contain no null or blank `item_unique_id` values. +- `character_parcels`, `character_parcels_containers`, and `inventory_snapshots` contain no null or blank `item_unique_id` values. +- `inventory`, `sharedbank`, and `trader` contain no duplicate `item_unique_id` groups. +- There are no cross-table duplicate live `item_unique_id` values across `inventory`, `sharedbank`, and `trader`. +- `offline_character_sessions` is empty after migration. +- `account.offline` has no rows left set to `1`. + +## Rollback Criteria + +Rollback instead of reopening the server if any of the following occur: + +- `world database:updates` fails. +- `world database:item-unique-ids --preflight` reports missing required schema. +- `world database:item-unique-ids --migrate` or `--verify` reports missing IDs, duplicate IDs, or stale offline session state. +- Bazaar listing, offline trader, offline buyer, or parcel retrieval smoke tests fail after migration. + +## Rollback Actions + +1. Keep the server in maintenance mode. +2. Restore the database backup taken before the rollout. +3. Redeploy the previous server build. +4. Confirm login, trader, buyer, and parcel behavior on the restored build before reopening the server. + +## Notes + +- `world database:item-unique-ids --keep-trading-state` exists for non-production diagnostics only. Do not use it during production cutover. +- The migration command expects schema updates to have been applied first. If required tables or columns are missing, the command fails and should not be bypassed. 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 5baa4eebe..a8f5d6cce 100644 --- a/loginserver/client.h +++ b/loginserver/client.h @@ -25,6 +25,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 b760eb7fe..01fa26c74 100644 --- a/loginserver/login_types.h +++ b/loginserver/login_types.h @@ -83,6 +83,11 @@ struct PlayEverquestResponse { uint32 server_number; }; +struct CancelOfflineTrader { + LoginBaseMessage base_header; + int16_t unk; +}; + #pragma pack() enum LSClientVersion { @@ -158,11 +163,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. - }; + }; } #pragma pack(pop) diff --git a/loginserver/login_util/login_opcodes_sod.conf b/loginserver/login_util/login_opcodes_sod.conf index cdc856d2c..9a21a8dcc 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 diff --git a/loginserver/main.cpp b/loginserver/main.cpp index 2aae37528..102c191ee 100644 --- a/loginserver/main.cpp +++ b/loginserver/main.cpp @@ -12,7 +12,6 @@ #include "loginserver/login_server.h" #include "loginserver/loginserver_command_handler.h" #include "loginserver/loginserver_webserver.h" - #include #include diff --git a/loginserver/world_server.cpp b/loginserver/world_server.cpp index 85b9d6c74..4187f7043 100644 --- a/loginserver/world_server.cpp +++ b/loginserver/world_server.cpp @@ -52,6 +52,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; @@ -299,6 +305,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; @@ -775,3 +785,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 94ed4b4a7..f48f4f085 100644 --- a/loginserver/world_server.h +++ b/loginserver/world_server.h @@ -58,6 +58,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 fdf917e7e..bbe73ddd4 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 1885b1669..20f4df309 100644 --- a/loginserver/world_server_manager.h +++ b/loginserver/world_server_manager.h @@ -16,6 +16,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..b14f9d230 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 diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index 80be6aeb8..5753b3ddd 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -9,6 +9,7 @@ set(world_sources cli/cli_database_concurrency.cpp cli/cli_database_dump.cpp cli/cli_database_get_schema.cpp + cli/cli_database_item_unique_ids.cpp cli/cli_database_set_account_status.cpp cli/cli_database_updates.cpp cli/cli_database_version.cpp diff --git a/world/cli/cli_database_item_unique_ids.cpp b/world/cli/cli_database_item_unique_ids.cpp new file mode 100644 index 000000000..660b87341 --- /dev/null +++ b/world/cli/cli_database_item_unique_ids.cpp @@ -0,0 +1,54 @@ +#include "world/world_server_cli.h" + +#include "world/worlddb.h" + +#include + +void WorldserverCLI::DatabaseItemUniqueIds(int argc, char **argv, argh::parser &cmd, std::string &description) +{ + description = "Runs item_unique_id preflight, migration, and verification tasks"; + + std::vector arguments = {}; + std::vector options = { + "--preflight", + "--migrate", + "--verify", + "--verbose", + "--keep-trading-state", + }; + + if (cmd[{"-h", "--help"}]) { + return; + } + + EQEmuCommand::ValidateCmdInput(arguments, options, cmd, argc, argv); + + const bool verbose = cmd[{"-v", "--verbose"}]; + const bool migrate = cmd[{"--migrate"}]; + const bool verify = cmd[{"--verify"}]; + const bool preflight = cmd[{"--preflight"}] || (!migrate && !verify); + const bool clear_trading_state = !cmd[{"--keep-trading-state"}]; + bool success = true; + + if (preflight) { + success = database.PreflightItemUniqueIdMigration(verbose) && success; + } + + if (migrate) { + if (!database.MigrateItemUniqueIdData(clear_trading_state, verbose)) { + LogError("Item unique id migration failed verification"); + success = false; + } + } + + if (verify) { + if (!database.VerifyItemUniqueIdMigration(verbose)) { + LogError("Item unique id verification failed"); + success = false; + } + } + + if (!success) { + std::exit(1); + } +} diff --git a/world/client.cpp b/world/client.cpp index 555a0055a..e65b6d801 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -2403,7 +2403,7 @@ bool Client::StoreCharacter( e.ornament_icon = inst->GetOrnamentationIcon(); e.ornament_idfile = inst->GetOrnamentationIDFile(); e.ornament_hero_model = inst->GetOrnamentHeroModel(); - e.guid = inst->GetSerialNumber(); + e.item_unique_id = inst->GetUniqueID(); v.emplace_back(e); } diff --git a/world/cliententry.cpp b/world/cliententry.cpp index 6b37f7b64..03486e07b 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 97c5e4002..3a0bd1e0d 100644 --- a/world/cliententry.h +++ b/world/cliententry.h @@ -13,7 +13,8 @@ typedef enum { Online, CharSelect, Zoning, - InZone + InZone, + OfflineMode } CLE_Status; static const char *CLEStatusString[] = { @@ -22,7 +23,8 @@ static const char *CLEStatusString[] = { "Online", "CharSelect", "Zoning", - "InZone" + "InZone", + "OfflineMode" }; class ZoneServer; @@ -102,6 +104,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(); } @@ -134,25 +140,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 1aa59930e..e29ccbd8f 100644 --- a/world/clientlist.cpp +++ b/world/clientlist.cpp @@ -35,6 +35,7 @@ #include "world/zoneserver.h" #include +#include "../zone/string_ids.h" uint32 numplayers = 0; //this really wants to be a member variable of ClientList... @@ -427,7 +428,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 +445,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 +487,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 +517,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 +798,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 = TRADER; + } + else if (cle->GetBuyer()) { + rankstring = BUYER; + } + else if (cle->GetGM()) { if (cle->Admin() >= AccountStatus::GMImpossible) { rankstring = 5021; } else if (cle->Admin() >= AccountStatus::GMMgmt) { @@ -877,6 +898,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 +1664,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 cbf998c3c..364d40375 100644 --- a/world/console.cpp +++ b/world/console.cpp @@ -58,7 +58,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 f911004f8..0554fd4bb 100644 --- a/world/login_server.cpp +++ b/world/login_server.cpp @@ -4,6 +4,11 @@ #include "common/eqemu_logsys.h" #include "common/misc_functions.h" #include "common/packet_dump.h" +#include "common/repositories/account_repository.h" +#include "common/repositories/buyer_repository.h" +#include "common/repositories/character_data_repository.h" +#include "common/repositories/offline_character_sessions_repository.h" +#include "common/repositories/trader_repository.h" #include "common/servertalk.h" #include "common/strings.h" #include "common/version.h" @@ -43,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 [{}]", @@ -123,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 = ClientList::Instance()->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, @@ -152,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 @@ -164,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 || OfflineCharacterSessionsRepository::ExistsByAccountId(database, id)) { + LogDebug("User has an offline character for account_id [{0}]", utwr->lsaccountid); + utwrs->response = UserToWorldStatusOffilineTraderBuyer; + SendPacket(&outpack); + return; + } + if (RuleB(World, EnforceCharacterLimitAtLogin)) { if (ClientList::Instance()->IsAccountInGame(utwr->lsaccountid)) { LogDebug("User already online account_id [{0}]", utwr->lsaccountid); @@ -572,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; @@ -687,3 +712,108 @@ 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 session = OfflineCharacterSessionsRepository::GetByAccountId(database, id); + auto trader = TraderRepository::GetAccountZoneIdAndInstanceIdByAccountId(database, id); + uint32 zone_id = session.id ? session.zone_id : trader.char_zone_id; + int32 instance_id = session.id ? session.instance_id : trader.char_zone_instance_id; + uint32 character_id = session.id ? session.character_id : trader.character_id; + + if ((session.id || trader.id) && + ZSList::Instance()->IsZoneBootedByZoneIdAndInstanceId(zone_id, instance_id)) { + LogLoginserverDetail( + "Step 5a(1) - World Checked offline users zone/instance is booted. " + "Sending packet to zone id {} instance id {}", + zone_id, + instance_id); + + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineRequest; + ZSList::Instance()->SendPacketToBootedZones(&server_packet); + return; + } + + LogLoginserverDetail("Step 5b(1) - World determined offline users zone/instance is not booted. Ignoring zone."); + + LogLoginserverDetail("Step 5b(2) - World clearing users offline status from account table."); + database.TransactionBegin(); + AccountRepository::SetOfflineStatus(database, id, false); + OfflineCharacterSessionsRepository::DeleteByAccountId(database, id); + + LogLoginserverDetail("Step 5b(3) - World clearing trader and buyer tables."); + if (character_id) { + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = '{}'", character_id)); + BuyerRepository::DeleteBuyer(database, character_id); + } + + auto commit_result = database.TransactionCommit(); + if (!commit_result.Success()) { + database.TransactionRollback(); + LogError( + "Failed clearing offline session state for account [{}]: ({}) {}", + id, + commit_result.ErrorNumber(), + commit_result.ErrorMessage() + ); + utwrs->response = UserToWorldStatusWorldUnavail; + } + + server_packet.opcode = ServerOP_UsertoWorldCancelOfflineResponse; + SendPacket(&server_packet); +} diff --git a/world/login_server.h b/world/login_server.h index e77366411..c2d2f2022 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/world_boot.cpp b/world/world_boot.cpp index 337e5d133..a36c904d9 100644 --- a/world/world_boot.cpp +++ b/world/world_boot.cpp @@ -634,4 +634,3 @@ void WorldBoot::SendDiscordMessage(int webhook_id, const std::string &message) safe_delete(pack); } } - diff --git a/world/world_server_cli.cpp b/world/world_server_cli.cpp index 50b16f4e4..cf946e722 100644 --- a/world/world_server_cli.cpp +++ b/world/world_server_cli.cpp @@ -23,6 +23,7 @@ void WorldserverCLI::CommandHandler(int argc, char **argv) function_map["database:schema"] = &WorldserverCLI::DatabaseGetSchema; function_map["database:dump"] = &WorldserverCLI::DatabaseDump; function_map["database:updates"] = &WorldserverCLI::DatabaseUpdates; + function_map["database:item-unique-ids"] = &WorldserverCLI::DatabaseItemUniqueIds; function_map["test:test"] = &WorldserverCLI::TestCommand; function_map["test:colors"] = &WorldserverCLI::TestColors; function_map["test:expansion"] = &WorldserverCLI::ExpansionTestCommand; diff --git a/world/world_server_cli.h b/world/world_server_cli.h index 4fa4b434e..1692e2041 100644 --- a/world/world_server_cli.h +++ b/world/world_server_cli.h @@ -17,6 +17,7 @@ public: static void DatabaseGetSchema(int argc, char **argv, argh::parser &cmd, std::string &description); static void DatabaseDump(int argc, char **argv, argh::parser &cmd, std::string &description); static void DatabaseUpdates(int argc, char **argv, argh::parser &cmd, std::string &description); + static void DatabaseItemUniqueIds(int argc, char **argv, argh::parser &cmd, std::string &description); static void TestCommand(int argc, char **argv, argh::parser &cmd, std::string &description); static void TestColors(int argc, char **argv, argh::parser &cmd, std::string &description); static void ExpansionTestCommand(int argc, char **argv, argh::parser &cmd, std::string &description); diff --git a/world/worlddb.cpp b/world/worlddb.cpp index b03478e8b..e347a5e60 100644 --- a/world/worlddb.cpp +++ b/world/worlddb.cpp @@ -876,7 +876,7 @@ bool WorldDatabase::GetCharSelInventory( continue; } - EQ::ItemInstance *inst = content_db.CreateBaseItem(item, e.charges); + EQ::ItemInstance *inst = content_db.CreateBaseItem(item, e.charges, e.item_unique_id); if (!inst) { continue; diff --git a/world/zonelist.cpp b/world/zonelist.cpp index 1f4ae80fc..8e0337d22 100644 --- a/world/zonelist.cpp +++ b/world/zonelist.cpp @@ -1019,3 +1019,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 9263a1e90..77ab7ae5f 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 8e6c7e72e..66e11d815 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1675,21 +1675,23 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { break; } case ServerOP_BazaarPurchase: { - auto in = (BazaarPurchaseMessaging_Struct *)pack->pBuffer; - if (in->trader_buy_struct.trader_id <= 0) { - LogTrading( - "World Message [{}] received with invalid trader_id [{}]", - "ServerOP_BazaarPurchase", - in->trader_buy_struct.trader_id - ); - return; + auto in = reinterpret_cast(pack->pBuffer); + switch (in->transaction_status) { + case BazaarPurchaseBuyerCompleteSendToSeller: { + ZSList::Instance()->SendPacket(in->trader_zone_id, in->trader_zone_instance_id, pack); + break; + } + case BazaarPurchaseTraderFailed: + case BazaarPurchaseSuccess: { + ZSList::Instance()->SendPacket(in->buyer_zone_id, in->buyer_zone_instance_id, pack); + break; + } + default: { + LogError( + "ServerOP_BazaarPurchase received with no corresponding action for [{}]", + in->transaction_status); + } } - - auto trader = ClientList::Instance()->FindCLEByCharacterID(in->trader_buy_struct.trader_id); - if (trader) { - ZSList::Instance()->SendPacket(trader->zone(), trader->instance(), pack); - } - break; } case ServerOP_BuyerMessaging: { @@ -1725,9 +1727,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::Instance()->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 4e85e9373..c4c1d3c6b 100644 --- a/zone/api_service.cpp +++ b/zone/api_service.cpp @@ -60,7 +60,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 05cb01991..ffcddf76d 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -498,10 +498,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); @@ -688,6 +688,7 @@ Client::Client(EQStreamInterface *ieqs) : Mob( m_parcels.clear(); m_buyer_id = 0; + m_offline = false; SetBotPulling(false); SetBotPrecombat(false); @@ -716,10 +717,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); } } @@ -731,11 +734,11 @@ Client::~Client() { if (merc) merc->Depop(); - if(IsTrader()) { + if(IsTrader() && !IsOffline()) { TraderEndTrader(); } - if(IsBuyer()) { + if(IsBuyer() && !IsOffline()) { ToggleBuyerMode(false); } @@ -767,7 +770,9 @@ Client::~Client() { if(isgrouped && !bZoning && is_zone_loaded) LeaveGroup(); - UpdateWho(2); + if (!IsOffline() && !IsTrader()) { + UpdateWho(2); + } if(IsHoveringForRespawn()) { @@ -2143,6 +2148,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; @@ -2211,7 +2219,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) { @@ -2527,6 +2535,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; @@ -9043,11 +9052,11 @@ void Client::QuestReward(Mob* target, const QuestReward_Struct &reward, bool fac void Client::CashReward(uint32 copper, uint32 silver, uint32 gold, uint32 platinum) { - auto outapp = std::make_unique(OP_CashReward, sizeof(CashReward_Struct)); + auto outapp = std::make_unique(OP_CashReward, static_cast(sizeof(CashReward_Struct))); auto outbuf = reinterpret_cast(outapp->pBuffer); - outbuf->copper = copper; - outbuf->silver = silver; - outbuf->gold = gold; + outbuf->copper = copper; + outbuf->silver = silver; + outbuf->gold = gold; outbuf->platinum = platinum; AddMoneyToPP(copper, silver, gold, platinum); @@ -12689,13 +12698,11 @@ uint16 Client::GetSkill(EQ::skills::SkillType skill_id) const return 0; } -void Client::RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity) +bool Client::RemoveItemByItemUniqueId(const std::string &item_unique_id, uint32 quantity) { - EQ::ItemInstance *item = nullptr; - - uint32 removed_count = 0; - - const auto& slot_ids = GetInventorySlots(); + EQ::ItemInstance *item = nullptr; + uint32 removed_count = 0; + const auto &slot_ids = GetInventorySlots(); for (const int16& slot_id : slot_ids) { if (removed_count == quantity) { @@ -12703,21 +12710,27 @@ void Client::RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity) } item = GetInv().GetItem(slot_id); - if (item && item->GetSerialNumber() == serial_number) { + if (item && item->GetUniqueID().compare(item_unique_id) == 0) { uint32 charges = item->IsStackable() ? item->GetCharges() : 0; uint32 stack_size = std::max(charges, static_cast(1)); - if ((removed_count + stack_size) <= quantity) { + if (removed_count + stack_size <= quantity) { removed_count += stack_size; - DeleteItemInInventory(slot_id, charges, true); + if (DeleteItemInInventory(slot_id, charges, true)) { + return true; + } } else { - uint32 amount_left = (quantity - removed_count); + uint32 amount_left = quantity - removed_count; if (amount_left > 0 && stack_size >= amount_left) { removed_count += amount_left; - DeleteItemInInventory(slot_id, amount_left, true); + if (DeleteItemInInventory(slot_id, amount_left, true)) { + return true; + } } } } } + + return false; } void Client::SendTopLevelInventory() diff --git a/zone/client.h b/zone/client.h index 6a1217904..559eed28c 100644 --- a/zone/client.h +++ b/zone/client.h @@ -303,7 +303,7 @@ public: void Trader_CustomerBrowsing(Client *Customer); void TraderEndTrader(); - void TraderPriceUpdate(const EQApplicationPacket *app); + void TraderUpdateItem(const EQApplicationPacket *app); void SendBazaarDone(uint32 trader_id); void SendBulkBazaarTraders(); void SendBulkBazaarBuyers(); @@ -340,7 +340,7 @@ public: void SendTraderPacket(Client* trader, uint32 Unknown72 = 51); void SendBuyerPacket(Client* Buyer); void SendBuyerToBarterWindow(Client* buyer, uint32 action); - GetItems_Struct* GetTraderItems(); + GetBazaarItems_Struct* GetTraderItems(); void SendBazaarWelcome(); void SendBarterWelcome(); void DyeArmor(EQ::TintProfile* dye); @@ -361,15 +361,17 @@ public: void SendColoredText(uint32 color, std::string message); void SendTraderItem(uint32 item_id,uint16 quantity, TraderRepository::Trader &trader); void DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria); - uint16 FindTraderItem(int32 SerialNumber,uint16 Quantity); - uint32 FindTraderItemSerialNumber(int32 ItemID); - EQ::ItemInstance* FindTraderItemBySerialNumber(int32 SerialNumber); - void FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client* customer, uint16 trader_slot); - void NukeTraderItem(uint16 slot, int16 charges, int16 quantity, Client* customer, uint16 trader_slot, int32 serial_number, int32 item_id = 0); + uint16 FindTraderItem(std::string &SerialNumber,uint16 Quantity); + EQ::ItemInstance* FindTraderItemByUniqueID(std::string &unique_id); + EQ::ItemInstance* FindTraderItemByUniqueID(const char* unique_id); + std::vector FindTraderItemsByUniqueID(const char* unique_id); + void FindAndNukeTraderItem(std::string &item_unique_id, int16 quantity, Client* customer, uint16 trader_slot); + void NukeTraderItem(uint16 slot, int16 charges, int16 quantity, Client* customer, uint16 trader_slot, const std::string &serial_number, int32 item_id = 0); void ReturnTraderReq(const EQApplicationPacket* app,int16 traderitemcharges, uint32 itemid = 0); void TradeRequestFailed(const EQApplicationPacket* app); - void BuyTraderItem(TraderBuy_Struct* tbs, Client* trader, const EQApplicationPacket* app); - void BuyTraderItemOutsideBazaar(TraderBuy_Struct* tbs, const EQApplicationPacket* app); + void TradeRequestFailed(TraderBuy_Struct &in); + void BuyTraderItem(const EQApplicationPacket* app); + void BuyTraderItemFromBazaarWindow(const EQApplicationPacket* app); void FinishTrade( Mob *with, bool finalizer = false, @@ -401,13 +403,22 @@ 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); - bool IsThereACustomer() const { return customer_id ? true : false; } + bool IsThereACustomer() const { return customer_id ? true : false; } uint32 GetCustomerID() { return customer_id; } - void SetCustomerID(uint32 id) { customer_id = id; } + void SetCustomerID(uint32 id) { customer_id = id; } + void ClearTraderMerchantList() { m_trader_merchant_list.clear(); } + void AddDataToMerchantList(int16 slot_id, uint32 item_id, int32 quantity, const std::string &item_unique_id); + int16 GetNextFreeSlotFromMerchantList(); + std::tuple GetDataFromMerchantListByMerchantSlotId(int16 slot_id); + int16 GetSlotFromMerchantListByItemUniqueId(const std::string &unique_id); + std::pair> GetDataFromMerchantListByItemUniqueId(const std::string &unique_id); + std::map>* GetTraderMerchantList() { return &m_trader_merchant_list; } void SetBuyerID(uint32 id) { m_buyer_id = id; } uint32 GetBuyerID() { return m_buyer_id; } @@ -486,7 +497,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(); @@ -576,8 +592,8 @@ public: void DisableAreaRegens(); void ServerFilter(SetServerFilter_Struct* filter); - void BulkSendTraderInventory(uint32 char_id); - void SendSingleTraderItem(uint32 char_id, int serial_number); + void BulkSendTraderInventory(uint32 character_id); + void SendSingleTraderItem(uint32 char_id, const std::string &serial_number); void BulkSendMerchantInventory(int merchant_id, int npcid); inline uint8 GetLanguageSkill(uint8 language_id) const { return m_pp.languages[language_id]; } @@ -1134,13 +1150,13 @@ public: bool FindNumberOfFreeInventorySlotsWithSizeCheck(std::vector items); bool PushItemOnCursor(const EQ::ItemInstance& inst, bool client_update = false); void SendCursorBuffer(); - void DeleteItemInInventory(int16 slot_id, int16 quantity = 0, bool client_update = false, bool update_db = true); + bool DeleteItemInInventory(int16 slot_id, int16 quantity = 0, bool client_update = false, bool update_db = true); uint32 CountItem(uint32 item_id); void ResetItemCooldown(uint32 item_id); void SetItemCooldown(uint32 item_id, bool use_saved_timer = false, uint32 in_seconds = 1); uint32 GetItemCooldown(uint32 item_id); void RemoveItem(uint32 item_id, uint32 quantity = 1); - void RemoveItemBySerialNumber(uint32 serial_number, uint32 quantity = 1); + bool RemoveItemByItemUniqueId(const std::string &item_unique_id, uint32 quantity = 1); bool SwapItem(MoveItem_Struct* move_in); void SwapItemResync(MoveItem_Struct* move_slots); void PutLootInInventory(int16 slot_id, const EQ::ItemInstance &inst, LootItem** bag_item_data = 0); @@ -2093,7 +2109,9 @@ private: uint8 mercSlot; // selected merc slot time_t m_trader_transaction_date; 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; @@ -2423,4 +2441,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; + m_is_afk = in.m_is_afk; + m_is_idle = in.m_is_idle; + m_is_manual_afk = in.m_is_manual_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()); + } }; diff --git a/zone/client_evolving_items.cpp b/zone/client_evolving_items.cpp index 97aa3213b..1a416499f 100644 --- a/zone/client_evolving_items.cpp +++ b/zone/client_evolving_items.cpp @@ -388,7 +388,7 @@ bool Client::DoEvolveCheckProgression(EQ::ItemInstance &inst) PlayerEvent::EvolveItem e{}; - RemoveItemBySerialNumber(inst.GetSerialNumber()); + RemoveItemByItemUniqueId(inst.GetUniqueID()); EvolvingItemsManager::Instance()->LoadPlayerEvent(inst, e); e.status = "Evolved Item due to obtaining progression - Old Evolve Item removed from inventory."; RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e); @@ -508,7 +508,7 @@ void Client::DoEvolveTransferXP(const EQApplicationPacket *app) PlayerEvent::EvolveItem e{}; - RemoveItemBySerialNumber(inst_from->GetSerialNumber()); + RemoveItemByItemUniqueId(inst_from->GetUniqueID()); EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_from, e); e.status = "Transfer XP - Original FROM Evolve Item removed from inventory."; RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e); @@ -518,7 +518,7 @@ void Client::DoEvolveTransferXP(const EQApplicationPacket *app) e.status = "Transfer XP - Updated FROM item placed in inventory."; RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e); - RemoveItemBySerialNumber(inst_to->GetSerialNumber()); + RemoveItemByItemUniqueId(inst_to->GetUniqueID()); EvolvingItemsManager::Instance()->LoadPlayerEvent(*inst_to, e); e.status = "Transfer XP - Original TO Evolve Item removed from inventory."; RecordPlayerEventLog(PlayerEvent::EVOLVE_ITEM, e); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 92fdcfe57..e0214ca5e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -25,6 +25,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "common/raid.h" #include "common/rdtsc.h" #include "common/repositories/account_repository.h" +#include "common/repositories/character_offline_transactions_repository.h" +#include "common/repositories/offline_character_sessions_repository.h" #include "common/repositories/adventure_members_repository.h" #include "common/repositories/buyer_buy_lines_repository.h" #include "common/repositories/character_corpses_repository.h" @@ -309,6 +311,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; @@ -738,7 +741,7 @@ void Client::CompleteConnect() if (is_first_login) { e.first_login = time(nullptr); - TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = '{}'", CharacterID())); BuyerRepository::DeleteBuyer(database, CharacterID()); LogTradingDetail( "Removed trader abd buyer entries for Character ID {} on first logon to ensure table consistency.", @@ -762,6 +765,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(); } @@ -805,7 +856,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()) { @@ -15341,10 +15392,10 @@ void Client::Handle_OP_Trader(const EQApplicationPacket *app) TraderStartTrader(app); break; } - case PriceUpdate: - case ItemMove: { - LogTrading("Trader Price Update"); - TraderPriceUpdate(app); + case ItemMove: + case PriceUpdate:{ + LogTrading("Trader item updated - removed, added or price change"); + TraderUpdateItem(app); break; } case EndTransaction: { @@ -15363,7 +15414,6 @@ void Client::Handle_OP_Trader(const EQApplicationPacket *app) break; } default: { - LogError("Unknown size for OP_Trader: [{}]\n", app->size); } } } @@ -15374,28 +15424,12 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) // // Client has elected to buy an item from a Trader // - auto in = (TraderBuy_Struct *) app->pBuffer; - if (RuleB(Bazaar, UseAlternateBazaarSearch) && in->trader_id >= TraderRepository::TRADER_CONVERT_ID) { - auto trader = TraderRepository::GetTraderByInstanceAndSerialnumber( - database, - in->trader_id - TraderRepository::TRADER_CONVERT_ID, - in->serial_number - ); - - if (!trader.trader_id) { - LogTrading("Unable to convert trader id for {} and serial number {}. Trader Buy aborted.", - in->trader_id - TraderRepository::TRADER_CONVERT_ID, - in->serial_number - ); - return; - } - - in->trader_id = trader.trader_id; - strn0cpy(in->seller_name, trader.trader_name.c_str(), sizeof(in->seller_name)); - } - - auto trader = entity_list.GetClientByID(in->trader_id); + auto in = (TraderBuy_Struct *) app->pBuffer; + auto item_unique_id = std::string(in->item_unique_id); + auto trader_details = TraderRepository::GetTraderByItemUniqueNumber(database, item_unique_id); + auto trader = entity_list.GetClientByID(in->trader_id); + strn0cpy(in->seller_name, trader_details.trader_name.c_str(), sizeof(in->seller_name)); switch (in->method) { case BazaarByVendor: { @@ -15405,9 +15439,9 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) in->trader_id, in->item_id, in->quantity, - in->serial_number + in->item_unique_id ); - BuyTraderItem(in, trader, app); + BuyTraderItem(app); } break; } @@ -15431,9 +15465,9 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) in->trader_id, in->item_id, in->quantity, - in->serial_number + in->item_unique_id ); - BuyTraderItemOutsideBazaar(in, app); + BuyTraderItemFromBazaarWindow(app); break; } case BazaarByDirectToInventory: { @@ -15456,7 +15490,7 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) in->trader_id, in->item_id, in->quantity, - in->serial_number + in->item_unique_id ); Message( Chat::Yellow, @@ -15467,6 +15501,9 @@ void Client::Handle_OP_TraderBuy(const EQApplicationPacket *app) TradeRequestFailed(app); break; } + default: { + + } } } @@ -15557,17 +15594,18 @@ void Client::Handle_OP_TraderShop(const EQApplicationPacket *app) switch (in->Code) { case ClickTrader: { LogTrading("Handle_OP_TraderShop case ClickTrader [{}]", in->Code); - auto outapp = - std::make_unique(OP_TraderShop, static_cast(sizeof(TraderClick_Struct)) + auto outapp = std::make_unique( + OP_TraderShop, + static_cast(sizeof(TraderClick_Struct)) ); auto data = (TraderClick_Struct *) outapp->pBuffer; - auto trader_client = entity_list.GetClientByID(in->TraderID); + auto trader = entity_list.GetClientByID(in->TraderID); - if (trader_client) { - data->Approval = trader_client->WithCustomer(GetID()); + if (trader) { + data->Approval = trader->WithCustomer(GetID()); LogTrading("Client::Handle_OP_TraderShop: Shop Request ([{}]) to ([{}]) with Approval: [{}]", GetCleanName(), - trader_client->GetCleanName(), + trader->GetCleanName(), data->Approval ); } @@ -15575,6 +15613,9 @@ void Client::Handle_OP_TraderShop(const EQApplicationPacket *app) LogTrading("Client::Handle_OP_TraderShop: entity_list.GetClientByID(tcs->traderid)" " returned a nullptr pointer" ); + auto outapp = new EQApplicationPacket(OP_ShopEndConfirm); + QueuePacket(outapp); + safe_delete(outapp); return; } @@ -15584,8 +15625,9 @@ void Client::Handle_OP_TraderShop(const EQApplicationPacket *app) QueuePacket(outapp.get()); if (data->Approval) { - BulkSendTraderInventory(trader_client->CharacterID()); - trader_client->Trader_CustomerBrowsing(this); + ClearTraderMerchantList(); + BulkSendTraderInventory(trader->CharacterID()); + trader->Trader_CustomerBrowsing(this); SetTraderID(in->TraderID); LogTrading("Client::Handle_OP_TraderShop: Trader Inventory Sent to [{}] from [{}]", GetID(), @@ -17275,3 +17317,88 @@ 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); + } + } + + 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); + + bool session_ready = true; + const auto previous_entity_id = GetID(); + const auto next_entity_id = offline_client->GetID(); + const auto mode = IsBuyer() ? std::string("buyer") : std::string("trader"); + + database.TransactionBegin(); + + if (IsBuyer()) { + offline_client->SetBuyerID(offline_client->CharacterID()); + session_ready = BuyerRepository::UpdateBuyerEntityID(database, CharacterID(), previous_entity_id, next_entity_id); + } + else { + offline_client->SetTrader(true); + session_ready = TraderRepository::UpdateEntityId(database, CharacterID(), previous_entity_id, next_entity_id); + } + + if (session_ready) { + session_ready = OfflineCharacterSessionsRepository::Upsert( + database, + AccountID(), + CharacterID(), + mode, + GetZoneID(), + GetInstanceID(), + next_entity_id + ); + } + + if (session_ready) { + AccountRepository::SetOfflineStatus(database, AccountID(), true); + auto commit_result = database.TransactionCommit(); + session_ready = commit_result.Success(); + if (!session_ready) { + LogError( + "Failed committing offline {} activation for character [{}] account [{}]: ({}) {}", + mode, + CharacterID(), + AccountID(), + commit_result.ErrorNumber(), + commit_result.ErrorMessage() + ); + } + } + + if (!session_ready) { + database.TransactionRollback(); + entity_list.RemoveMob(offline_client->CastToMob()->GetID()); + return; + } + + SetOffline(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 479e49d51..d0ebfc1a3 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -168,7 +168,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); @@ -197,7 +197,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()) @@ -578,7 +578,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); @@ -589,7 +589,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; @@ -607,7 +607,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); @@ -617,7 +617,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 c1a7e1796..0cb29dbbf 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -4995,15 +4995,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/gm_commands/show/show_inventory.cpp b/zone/gm_commands/show/show_inventory.cpp index 4abfd6a68..40bc475fc 100644 --- a/zone/gm_commands/show/show_inventory.cpp +++ b/zone/gm_commands/show/show_inventory.cpp @@ -185,7 +185,7 @@ void ShowInventory(Client *c, const Seperator *sep) scope_bit & peekWorld ? EQ::invslot::WORLD_BEGIN + index_main : index_main, linker.GenerateLink(), item_data->ID, - inst_main->GetSerialNumber(), + inst_main->GetUniqueID().c_str(), inst_main->IsStackable() && inst_main->GetCharges() > 0 ? fmt::format( " (Stack of {})", @@ -254,7 +254,7 @@ void ShowInventory(Client *c, const Seperator *sep) sub_index, linker.GenerateLink(), item_data->ID, - inst_sub->GetSerialNumber(), + inst_sub->GetUniqueID().c_str(), ( inst_sub->IsStackable() && inst_sub->GetCharges() > 0 ? fmt::format( diff --git a/zone/guild_mgr.cpp b/zone/guild_mgr.cpp index d13d04c1a..a3e90ac46 100644 --- a/zone/guild_mgr.cpp +++ b/zone/guild_mgr.cpp @@ -427,10 +427,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); @@ -653,17 +654,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); } @@ -1518,14 +1526,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 c552ce194..49730b654 100644 --- a/zone/guild_mgr.h +++ b/zone/guild_mgr.h @@ -88,7 +88,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/inventory.cpp b/zone/inventory.cpp index cb28aeb2c..9ea858570 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -650,8 +650,7 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, // put item into inventory if (to_slot == EQ::invslot::slotCursor) { - PushItemOnCursor(*inst); - SendItemPacket(EQ::invslot::slotCursor, inst, ItemPacketLimbo); + PushItemOnCursor(*inst, true); } else { PutItemInInventory(to_slot, *inst, true); } @@ -958,7 +957,7 @@ void Client::SendCursorBuffer() } // Remove item from inventory -void Client::DeleteItemInInventory(int16 slot_id, int16 quantity, bool client_update, bool update_db) { +bool Client::DeleteItemInInventory(int16 slot_id, int16 quantity, bool client_update, bool update_db) { #if (EQDEBUG >= 5) LogDebug("DeleteItemInInventory([{}], [{}], [{}])", slot_id, quantity, (client_update) ? "true":"false"); #endif @@ -977,7 +976,7 @@ void Client::DeleteItemInInventory(int16 slot_id, int16 quantity, bool client_up QueuePacket(outapp); safe_delete(outapp); } - return; + return false; } uint64 evolve_id = m_inv[slot_id]->GetEvolveUniqueID(); @@ -1031,6 +1030,8 @@ void Client::DeleteItemInInventory(int16 slot_id, int16 quantity, bool client_up safe_delete(outapp); } } + + return true; } bool Client::PushItemOnCursor(const EQ::ItemInstance& inst, bool client_update) @@ -1055,6 +1056,16 @@ bool Client::PushItemOnCursor(const EQ::ItemInstance& inst, bool client_update) bool Client::PutItemInInventory(int16 slot_id, const EQ::ItemInstance& inst, bool client_update) { LogInventory("Putting item [{}] ([{}]) into slot [{}]", inst.GetItem()->Name, inst.GetItem()->ID, slot_id); + if (inst.GetUniqueID().empty()) { + auto item_unique_id = std::string(inst.GetUniqueID()); + if (!database.EnsureItemUniqueId(item_unique_id)) { + LogError("Failed to reserve item_unique_id for item [{}] ([{}])", inst.GetItem()->Name, inst.GetItem()->ID); + return false; + } + + const_cast(inst).SetUniqueID(item_unique_id); + } + if (slot_id == EQ::invslot::slotCursor) { // don't trust macros before conditional statements... return PushItemOnCursor(inst, client_update); } @@ -4674,19 +4685,112 @@ bool Client::HasItemOnCorpse(uint32 item_id) bool Client::PutItemInInventoryWithStacking(EQ::ItemInstance *inst) { auto free_id = GetInv().FindFirstFreeSlotThatFitsItem(inst->GetItem()); - if (inst->IsStackable()) { - if (TryStacking(inst, ItemPacketTrade, true, false)) { + + if (!inst->IsStackable()) { + if (free_id != INVALID_INDEX && + !EQ::ValueWithin(free_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) { + return PutItemInInventory(free_id, *inst, true); + } + + return false; + } + + struct temp { + int16 slot_id; + int32 quantity; + }; + + std::vector queue; + auto quantity = inst->GetCharges(); + + for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + auto inv_inst = GetInv().GetItem(i); + if (!inv_inst) { + LogError("Found a slot {} in general inventory", i); + inst->SetCharges(quantity); + PutItemInInventory(i, *inst, true); return true; } - } - // Protect equipment slots (0-22) from being overwritten - if (free_id != INVALID_INDEX && !EQ::ValueWithin(free_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) { - if (PutItemInInventory(free_id, *inst, true)) { - return true; + + int16 base_slot_id = EQ::InventoryProfile::CalcSlotId(i, EQ::invbag::SLOT_BEGIN); + uint8 bag_size = inv_inst->GetItem()->BagSlots; + + for (uint8 bag_slot = EQ::invbag::SLOT_BEGIN; bag_slot < bag_size; bag_slot++) { + if (quantity == 0) { + break; + } + + auto bag_inst = GetInv().GetItem(base_slot_id + bag_slot); + if (!bag_inst && inv_inst->GetItem()->BagSize >= inst->GetItem()->Size) { + LogError("Found a parent {} base_slot_id {} bag_slot {} in bag", i, base_slot_id, bag_slot); + inst->SetCharges(quantity); + PutItemInInventory(base_slot_id + bag_slot, *inst, true); + return true; + } + + if (bag_inst && bag_inst->IsStackable() && bag_inst->GetID() == inst->GetID()) { + auto stack_size = bag_inst->GetItem()->StackSize; + auto bag_inst_quantity = bag_inst->GetCharges(); + int16 temp_slot = base_slot_id + bag_slot; + if (stack_size - bag_inst_quantity >= quantity) { + temp tmp = {temp_slot, quantity}; + queue.push_back(tmp); + quantity = 0; + LogError( + "Found an item parent {} base_slot_id {} bag_slot {} in bag with ENOUGH space", + i, + base_slot_id, + bag_slot + ); + break; + } + + if (stack_size - bag_inst_quantity > 0) { + temp tmp = {temp_slot, stack_size - bag_inst_quantity}; + queue.push_back(tmp); + quantity -= stack_size - bag_inst_quantity; + LogError( + "Found an item parent {} base_slot_id {} bag_slot {} in bag with SOME space", + i, + base_slot_id, + bag_slot + ); + } + } } } + + if (!queue.empty()) { + database.TransactionBegin(); + for (auto const &i: queue) { + auto bag_inst = GetInv().GetItem(i.slot_id); + if (!bag_inst) { + LogError("Client inventory error occurred. Character ID {} Slot_ID {}", CharacterID(), i.slot_id); + continue; + } + bag_inst->SetCharges(i.quantity + bag_inst->GetCharges()); + PutItemInInventory(i.slot_id, *bag_inst, true); + LogError("Write out data. Item {} quantity {} slot {}", bag_inst->GetItem()->Name, i.quantity, i.slot_id); + } + + database.TransactionCommit(); + } + + if (quantity == 0) { + LogError("Quantity was zero. All items placed in inventory."); + return true; + } + + inst->SetCharges(quantity); + if (free_id != INVALID_INDEX && + !EQ::ValueWithin(free_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END) && + PutItemInInventory(free_id, *inst, true)) { + return true; + } + + LogError("Could not find enough room"); return false; -}; +} bool Client::FindNumberOfFreeInventorySlotsWithSizeCheck(std::vector items) { diff --git a/zone/mob.h b/zone/mob.h index bd1e54971..4f5b27b73 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1954,4 +1954,103 @@ 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]; + } + } }; diff --git a/zone/parcels.cpp b/zone/parcels.cpp index 591f5127b..12281aa67 100644 --- a/zone/parcels.cpp +++ b/zone/parcels.cpp @@ -397,6 +397,7 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) parcel_out.sent_date = time(nullptr); parcel_out.quantity = quantity; parcel_out.item_id = inst->GetID(); + parcel_out.item_unique_id = inst->GetUniqueID(); parcel_out.char_id = send_to_client.at(0).char_id; parcel_out.slot_id = next_slot; parcel_out.evolve_amount = inst->GetEvolveCurrentAmount(); @@ -434,13 +435,14 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) std::vector all_entries{}; if (inst->IsNoneEmptyContainer()) { - for (auto const &kv: *inst->GetContents()) { + for (auto const &[slot, item]: *inst->GetContents()) { CharacterParcelsContainersRepository::CharacterParcelsContainers cpc{}; - cpc.parcels_id = result.id; - cpc.slot_id = kv.first; - cpc.item_id = kv.second->GetID(); - if (kv.second->IsAugmented()) { - auto augs = kv.second->GetAugmentIDs(); + cpc.parcels_id = result.id; + cpc.slot_id = slot; + cpc.item_id = item->GetID(); + cpc.item_unique_id = item->GetUniqueID(); + if (item->IsAugmented()) { + auto augs = item->GetAugmentIDs(); cpc.aug_slot_1 = augs.at(0); cpc.aug_slot_2 = augs.at(1); cpc.aug_slot_3 = augs.at(2); @@ -449,14 +451,15 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) cpc.aug_slot_6 = augs.at(5); } - cpc.quantity = kv.second->GetCharges() >= 0 ? kv.second->GetCharges() : 1; - cpc.evolve_amount = kv.second->GetEvolveCurrentAmount(); + cpc.quantity = item->GetCharges() >= 0 ? item->GetCharges() : 1; + cpc.evolve_amount = item->GetEvolveCurrentAmount(); + cpc.quantity = item->GetCharges() >= 0 ? item->GetCharges() : 1; all_entries.push_back(cpc); } CharacterParcelsContainersRepository::InsertMany(database, all_entries); } - RemoveItemBySerialNumber(inst->GetSerialNumber(), parcel_out.quantity == 0 ? 1 : parcel_out.quantity); + RemoveItemByItemUniqueId(inst->GetUniqueID(), parcel_out.quantity == 0 ? 1 : parcel_out.quantity); std::unique_ptr outapp(new EQApplicationPacket(OP_ShopSendParcel)); QueuePacket(outapp.get()); @@ -478,6 +481,7 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) e.from_player_name = parcel_out.from_name; e.to_player_name = send_to_client.at(0).character_name; e.item_id = parcel_out.item_id; + e.item_unique_id = parcel_out.item_unique_id; e.augment_1_id = parcel_out.aug_slot_1; e.augment_2_id = parcel_out.aug_slot_2; e.augment_3_id = parcel_out.aug_slot_3; @@ -494,6 +498,7 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) e.from_player_name = parcel_out.from_name; e.to_player_name = send_to_client.at(0).character_name; e.item_id = i.item_id; + e.item_unique_id = i.item_unique_id; e.augment_1_id = i.aug_slot_1; e.augment_2_id = i.aug_slot_2; e.augment_3_id = i.aug_slot_3; @@ -656,8 +661,9 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) } ); if (p != m_parcels.end()) { - uint32 item_id = parcel_in.parcel_item_id; - uint32 item_quantity = p->second.quantity; + uint32 item_id = parcel_in.parcel_item_id; + uint32 item_quantity = p->second.quantity; + std::string item_unique_id = p->second.item_unique_id; if (!item_id) { LogError( "Attempt to retrieve parcel with erroneous item id for client character id {}.", @@ -699,6 +705,7 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) break; } default: { + inst->SetUniqueID(item_unique_id); std::vector results{}; if (inst->IsClassBag() && inst->GetItem()->BagSlots > 0) { auto contents = inst->GetContents(); @@ -723,7 +730,7 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) } item->SetEvolveCurrentAmount(i.evolve_amount); - + item->SetUniqueID(i.item_unique_id); if (CheckLoreConflict(item->GetItem())) { if (RuleB(Parcel, DeleteOnDuplicate)) { MessageString(Chat::Yellow, PARCEL_DUPLICATE_DELETE, inst->GetItem()->Name); @@ -772,6 +779,7 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) PlayerEvent::ParcelRetrieve e{}; e.from_player_name = p->second.from_name; e.item_id = p->second.item_id; + e.item_unique_id = p->second.item_unique_id; e.augment_1_id = p->second.aug_slot_1; e.augment_2_id = p->second.aug_slot_2; e.augment_3_id = p->second.aug_slot_3; @@ -785,6 +793,7 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) for (auto const &i:results) { e.from_player_name = p->second.from_name; e.item_id = i.item_id; + e.item_unique_id = i.item_unique_id; e.augment_1_id = i.aug_slot_1; e.augment_2_id = i.aug_slot_2; e.augment_3_id = i.aug_slot_3; @@ -794,8 +803,6 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) e.quantity = i.quantity; e.sent_date = p->second.sent_date; RecordPlayerEventLog(PlayerEvent::PARCEL_RETRIEVE, e); - - } } } diff --git a/zone/string_ids.h b/zone/string_ids.h index 50fe61bfb..9b6783a7a 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -308,6 +308,7 @@ #define PLAYER_CHARMED 1461 //You lose control of yourself! #define TRADER_BUSY 1468 //That Trader is currently with a customer. Please wait until their transaction is finished. #define SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction. +#define HOW_CAN_YOU_BUY_MORE 1571 //%1 tells you, 'Your inventory appears full! How can you buy more?' #define DUPE_LORE_MERCHANT 1573 //%1 tells you, 'You already have the lore item, %2, on your person, on your shroud, in the bank, in a real estate, or as an augment in another item. You cannot have more than one of a particular lore item at a time.' #define QUEUED_TELL 2458 //[queued] #define QUEUE_TELL_FULL 2459 //[zoing and queue is full] @@ -414,6 +415,7 @@ #define MAX_ACTIVE_TASKS 6010 //Sorry %3, you already have the maximum number of active tasks. #define TASK_REQUEST_COOLDOWN_TIMER 6011 //Sorry, %3, but you can't request another task for %4 minutes and %5 seconds. #define FORAGE_MASTERY 6012 //Your forage mastery has enabled you to find something else! +#define BUYER 6056 //BUYER #define BUYER_WELCOME 6065 //There are %1 Buyers waiting to purchase your loot. Type /barter to search for them, or use /buyer to set up your own Buy Lines. #define BUYER_GREETING 6070 //%1 greets you, '%2' #define GUILD_BANK_CANNOT_DEPOSIT 6097 // Cannot deposit this item. Containers must be empty, and only one of each LORE and no NO TRADE or TEMPORARY items may be deposited. @@ -465,6 +467,8 @@ #define LDON_NO_LOCKPICK 7564 //You must have a lock pick in your inventory to do this. #define LDON_WAS_NOT_LOCKED 7565 //%1 was not locked. #define LDON_WAS_NOT_TRAPPED 7566 //%1 was not trapped +#define DUPLICATE_LORE 7623 //Transaction failed: Duplicate Lore Item! +#define INSUFFICIENT_FUNDS 7632 //Transaction failed: Insufficient funds! #define GAIN_SINGLE_AA_SINGLE_AA 8019 //You have gained an ability point! You now have %1 ability point. #define GAIN_SINGLE_AA_MULTI_AA 8020 //You have gained an ability point! You now have %1 ability points. #define GAIN_MULTI_AA_MULTI_AA 8021 //You have gained %1 ability point(s)! You now have %2 ability point(s). @@ -539,6 +543,7 @@ #define GROUP_INVITEE_NOT_FOUND 12268 //You must target a player or use /invite to invite someone to your group. #define GROUP_INVITEE_SELF 12270 //12270 You cannot invite yourself. #define ALREADY_IN_PARTY 12272 //That person is already in your party. +#define TRADER 12315 //TRADER #define TALKING_TO_SELF 12323 //Talking to yourself again? #define SPLIT_NO_GROUP 12328 //You are not in a group! Keep it all. #define NO_LONGER_HIDDEN 12337 //You are no longer hidden. diff --git a/zone/trading.cpp b/zone/trading.cpp index b5f26eb4d..fb5cf0a9a 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -25,6 +25,7 @@ #include "common/misc_functions.h" #include "common/repositories/buyer_buy_lines_repository.h" #include "common/repositories/buyer_repository.h" +#include "common/repositories/character_offline_transactions_repository.h" #include "common/repositories/trader_repository.h" #include "common/rulesys.h" #include "common/strings.h" @@ -754,21 +755,32 @@ bool Client::CheckTradeNonDroppable() void Client::TraderShowItems() { - auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_Struct)); - auto data = (Trader_Struct *) outapp->pBuffer; + std::stringstream ss{}; + cereal::BinaryOutputArchive ar(ss); - auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); - uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? - GetInv().GetLookup()->InventoryTypeSize.Bazaar : - trader_items.size(); - - for (int i = 0; i < item_limit; i++) { - data->item_cost[i] = trader_items.at(i).item_cost; - data->items[i] = ClientVersion() == EQ::versions::ClientVersion::RoF2 ? trader_items.at(i).item_sn - : trader_items.at(i).item_id; + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", CharacterID())); + if (trader_items.empty()) { + return; } - data->action = ListTraderItems; + TraderClientMessaging_Struct tcm{}; + tcm.action = ListTraderItems; + + for (auto const &t: trader_items) { + TraderItems_Struct items{}; + items.item_unique_id = t.item_unique_id; + items.item_id = t.item_id; + items.item_cost = t.item_cost; + + tcm.items.push_back(items); + } + + { ar(tcm); } + + uint32 packet_size = ss.str().length(); + auto outapp = new EQApplicationPacket(OP_Trader, packet_size); + + memcpy(outapp->pBuffer, ss.str().data(), packet_size); QueuePacket(outapp); safe_delete(outapp); @@ -811,95 +823,73 @@ void Client::Trader_CustomerBrowsing(Client *Customer) void Client::TraderStartTrader(const EQApplicationPacket *app) { uint32 max_items = GetInv().GetLookup()->InventoryTypeSize.Bazaar; - auto in = (ClickTrader_Struct *) app->pBuffer; auto inv = GetTraderItems(); bool trade_items_valid = true; std::vector trader_items{}; + ClickTraderNew_Struct in; - //Check inventory for no-trade items - for (auto i = 0; i < max_items; i++) { - if (inv->items[i] == 0 || inv->serial_number[i] == 0) { - continue; + EQ::Util::MemoryStreamReader ss(reinterpret_cast(app->pBuffer), app->size); + cereal::BinaryInputArchive ar(ss); + { + ar(in); + } + + uint32 slot_id = 0; + for (auto &i: in.items) { + auto const inst = FindTraderItemByUniqueID(i.unique_id); + if (!inst) { + trade_items_valid = false; + break; } - auto inst = FindTraderItemBySerialNumber(inv->serial_number[i]); if (inst) { if (inst->GetItem() && inst->GetItem()->NoDrop == 0) { Message( Chat::Red, fmt::format( "Item: {} is NODROP and found in a Trader's Satchel. Please remove and restart trader mode", - inst->GetItem()->Name - ).c_str() - ); + inst->GetItem()->Name) + .c_str()); TraderEndTrader(); safe_delete(inv); return; } } + + TraderRepository::Trader trader_item{}; + + trader_item.id = 0; + trader_item.char_entity_id = GetID(); + trader_item.character_id = CharacterID(); + trader_item.char_zone_id = GetZoneID(); + trader_item.char_zone_instance_id = GetInstanceID(); + trader_item.item_charges = inst->GetCharges(); + trader_item.item_cost = i.cost; + trader_item.item_id = inst->GetID(); + trader_item.item_unique_id = i.unique_id; + trader_item.slot_id = slot_id; + trader_item.listing_date = time(nullptr); + if (inst->IsAugmented()) { + auto augs = inst->GetAugmentIDs(); + trader_item.augment_one = augs.at(0); + trader_item.augment_two = augs.at(1); + trader_item.augment_three = augs.at(2); + trader_item.augment_four = augs.at(3); + trader_item.augment_five = augs.at(4); + trader_item.augment_six = augs.at(5); + } + + trader_items.emplace_back(trader_item); } - for (uint32 i = 0; i < max_items; i++) { - if (inv->serial_number[i] <= 0) { - continue; - } - - auto inst = FindTraderItemBySerialNumber(inv->serial_number[i]); - if (!inst) { - trade_items_valid = false; - break; - } - - auto it = std::find(std::begin(in->serial_number), std::end(in->serial_number), inv->serial_number[i]); - if (inst && it != std::end(in->serial_number)) { - inst->SetPrice(in->item_cost[i]); - TraderRepository::Trader trader_item{}; - - trader_item.id = 0; - trader_item.char_entity_id = GetID(); - trader_item.char_id = CharacterID(); - trader_item.char_zone_id = GetZoneID(); - trader_item.char_zone_instance_id = GetInstanceID(); - trader_item.item_charges = inst->GetCharges() == 0 ? 1 : inst->GetCharges(); - trader_item.item_cost = inst->GetPrice(); - trader_item.item_id = inst->GetID(); - trader_item.item_sn = in->serial_number[i]; - trader_item.slot_id = i; - trader_item.listing_date = time(nullptr); - if (inst->IsAugmented()) { - auto augs = inst->GetAugmentIDs(); - trader_item.aug_slot_1 = augs.at(0); - trader_item.aug_slot_2 = augs.at(1); - trader_item.aug_slot_3 = augs.at(2); - trader_item.aug_slot_4 = augs.at(3); - trader_item.aug_slot_5 = augs.at(4); - trader_item.aug_slot_6 = augs.at(5); - } - - trader_items.emplace_back(trader_item); - continue; - } - else if (inst) { - Message( - Chat::Red, - fmt::format( - "Item: {} has no price set. Please set a price and try again.", - inst->GetItem()->Name - ).c_str() - ); - trade_items_valid = false; - continue; - } - } - - if (!trade_items_valid) { + if (!trade_items_valid || trader_items.empty()) { Message(Chat::Red, "You are not able to become a trader at this time. Invalid item found."); TraderEndTrader(); safe_delete(inv); return; } - TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}';", CharacterID())); + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = {};", CharacterID())); TraderRepository::ReplaceMany(database, trader_items); safe_delete(inv); @@ -907,7 +897,7 @@ void Client::TraderStartTrader(const EQApplicationPacket *app) if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderStatus_Struct)); auto data = (TraderStatus_Struct *) outapp->pBuffer; - data->Code = TraderAck2; + data->Code = TraderAck2; QueuePacket(outapp); safe_delete(outapp); } @@ -916,6 +906,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()); } @@ -929,13 +920,14 @@ void Client::TraderEndTrader() } } - TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = {}", CharacterID())); SendBecomeTraderToWorld(this, TraderOff); SendTraderMode(TraderOff); WithCustomer(0); SetTrader(false); + UpdateWho(); } void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Trader &t) { @@ -954,12 +946,12 @@ void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Tr database.CreateItem( item, Quantity, - t.aug_slot_1, - t.aug_slot_2, - t.aug_slot_3, - t.aug_slot_4, - t.aug_slot_5, - t.aug_slot_6 + t.augment_one, + t.augment_two, + t.augment_three, + t.augment_four, + t.augment_five, + t.augment_six ) ); @@ -978,58 +970,50 @@ void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Tr } } -void Client::SendSingleTraderItem(uint32 char_id, int serial_number) +void Client::SendSingleTraderItem(uint32 character_id, const std::string &serial_number) { - auto inst = database.LoadSingleTraderItem(char_id, serial_number); + auto inst = database.LoadSingleTraderItem(character_id, serial_number); if (inst) { SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor? } } -void Client::BulkSendTraderInventory(uint32 char_id) +void Client::BulkSendTraderInventory(uint32 character_id) { const EQ::ItemData *item; - auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", char_id)); + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", character_id)); uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? GetInv().GetLookup()->InventoryTypeSize.Bazaar : trader_items.size(); - for (uint32 i = 0; i < item_limit; i++) { - if ((trader_items.at(i).item_id == 0) || (trader_items.at(i).item_cost == 0)) { + for (int16 i = 0; i < item_limit; i++) { + if (trader_items.at(i).item_id == 0 || trader_items.at(i).item_cost == 0) { continue; } - else { - item = database.GetItem(trader_items.at(i).item_id); - } + + item = database.GetItem(trader_items.at(i).item_id); if (item && (item->NoDrop != 0)) { std::unique_ptr inst( database.CreateItem( trader_items.at(i).item_id, trader_items.at(i).item_charges, - trader_items.at(i).aug_slot_1, - trader_items.at(i).aug_slot_2, - trader_items.at(i).aug_slot_3, - trader_items.at(i).aug_slot_4, - trader_items.at(i).aug_slot_5, - trader_items.at(i).aug_slot_6 + trader_items.at(i).augment_one, + trader_items.at(i).augment_two, + trader_items.at(i).augment_three, + trader_items.at(i).augment_four, + trader_items.at(i).augment_five, + trader_items.at(i).augment_six ) ); if (inst) { - inst->SetSerialNumber(trader_items.at(i).item_sn); - if (trader_items.at(i).item_charges > 0) { - inst->SetCharges(trader_items.at(i).item_charges); - } - - if (inst->IsStackable()) { - inst->SetMerchantCount(trader_items.at(i).item_charges); - inst->SetMerchantSlot(trader_items.at(i).item_sn); - } - + inst->SetUniqueID(trader_items.at(i).item_unique_id); + inst->SetMerchantCount(inst->IsStackable() ? inst->GetCharges() : 1); + inst->SetMerchantSlot(i + 1); inst->SetPrice(trader_items.at(i).item_cost); - SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); -// safe_delete(inst); + AddDataToMerchantList(i + 1, inst->GetID(), inst->GetMerchantCount(), inst->GetUniqueID()); + SendItemPacket(i + 1, inst.get(), ItemPacketMerchant); } else LogTrading("Client::BulkSendTraderInventory nullptr inst pointer"); @@ -1037,34 +1021,34 @@ void Client::BulkSendTraderInventory(uint32 char_id) } } -uint32 Client::FindTraderItemSerialNumber(int32 ItemID) { - - EQ::ItemInstance* item = nullptr; - uint16 SlotID = 0; - for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++){ - item = GetInv().GetItem(i); - if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel){ - for (int x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { - // we already have the parent bag and a contents iterator..why not just iterate the bag!?? - SlotID = EQ::InventoryProfile::CalcSlotId(i, x); - item = GetInv().GetItem(SlotID); - if (item) { - if (item->GetID() == ItemID) - return item->GetSerialNumber(); - } - } - } - } - LogTrading("Client::FindTraderItemSerialNumber Couldn't find item! Item ID [{}]", ItemID); - - return 0; -} - -EQ::ItemInstance *Client::FindTraderItemBySerialNumber(int32 SerialNumber) +EQ::ItemInstance *Client::FindTraderItemByUniqueID(std::string &unique_id) { EQ::ItemInstance *item = nullptr; int16 slot_id = 0; + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + item = GetInv().GetItem(i); + if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { + for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { + // we already have the parent bag and a contents iterator..why not just iterate the bag!?? + slot_id = EQ::InventoryProfile::CalcSlotId(i, x); + item = GetInv().GetItem(slot_id); + if (item && item->GetUniqueID().compare(unique_id) == 0) { + return item; + } + } + } + } + + LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id); + return nullptr; +} + +EQ::ItemInstance *Client::FindTraderItemByUniqueID(const char* unique_id) +{ + EQ::ItemInstance *item = nullptr; + int16 slot_id = 0; + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { item = GetInv().GetItem(i); if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { @@ -1073,7 +1057,7 @@ EQ::ItemInstance *Client::FindTraderItemBySerialNumber(int32 SerialNumber) slot_id = EQ::InventoryProfile::CalcSlotId(i, x); item = GetInv().GetItem(slot_id); if (item) { - if (item->GetSerialNumber() == SerialNumber) { + if (item->GetUniqueID().compare(unique_id) == 0) { return item; } } @@ -1081,17 +1065,39 @@ EQ::ItemInstance *Client::FindTraderItemBySerialNumber(int32 SerialNumber) } } - LogTrading("Couldn't find item! Serial No. was [{}]", SerialNumber); - + LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id); return nullptr; } +std::vector Client::FindTraderItemsByUniqueID(const char* unique_id) +{ + std::vector items{}; + EQ::ItemInstance *item = nullptr; + int16 slot_id = 0; -GetItems_Struct *Client::GetTraderItems() + for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { + item = GetInv().GetItem(i); + if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) { + for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) { + // we already have the parent bag and a contents iterator..why not just iterate the bag!?? + slot_id = EQ::InventoryProfile::CalcSlotId(i, x); + item = GetInv().GetItem(slot_id); + if (item && item->GetUniqueID().compare(unique_id) == 0) { + items.push_back(item); + } + } + } + } + + LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id); + return items; +} + +GetBazaarItems_Struct *Client::GetTraderItems() { const EQ::ItemInstance *item = nullptr; int16 slot_id = INVALID_INDEX; - auto gis = new GetItems_Struct{0}; + auto gis = new GetBazaarItems_Struct{0}; uint8 ndx = 0; for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) { @@ -1110,7 +1116,7 @@ GetItems_Struct *Client::GetTraderItems() if (item) { gis->items[ndx] = item->GetID(); - gis->serial_number[ndx] = item->GetSerialNumber(); + gis->serial_number[ndx] = item->GetUniqueID(); gis->charges[ndx] = item->GetCharges() == 0 ? 1 : item->GetCharges(); ndx++; } @@ -1120,7 +1126,7 @@ GetItems_Struct *Client::GetTraderItems() return gis; } -uint16 Client::FindTraderItem(int32 SerialNumber, uint16 Quantity){ +uint16 Client::FindTraderItem(std::string &serial_number, uint16 Quantity){ const EQ::ItemInstance* item= nullptr; uint16 SlotID = 0; @@ -1132,7 +1138,7 @@ uint16 Client::FindTraderItem(int32 SerialNumber, uint16 Quantity){ item = GetInv().GetItem(SlotID); - if (item && item->GetSerialNumber() == SerialNumber && + if (item && item->GetUniqueID().compare(serial_number) == 0 && (item->GetCharges() >= Quantity || (item->GetCharges() <= 0 && Quantity == 1))) { return SlotID; @@ -1141,7 +1147,7 @@ uint16 Client::FindTraderItem(int32 SerialNumber, uint16 Quantity){ } } LogTrading("Could NOT find a match for Item: [{}] with a quantity of: [{}] on Trader: [{}]\n", - SerialNumber , Quantity, GetName()); + serial_number , Quantity, GetName()); return 0; } @@ -1152,7 +1158,7 @@ void Client::NukeTraderItem( int16 quantity, Client *customer, uint16 trader_slot, - int32 serial_number, + const std::string &serial_number, int32 item_id ) { @@ -1171,7 +1177,8 @@ void Client::NukeTraderItem( tdis->unknown_000 = 0; tdis->trader_id = customer->GetID(); - tdis->item_id = serial_number; + tdis->item_id = Strings::ToUnsignedBigInt(serial_number); + strn0cpy(tdis->item_unique_id, serial_number.c_str(), sizeof(tdis->item_unique_id)); tdis->unknown_012 = 0; customer->QueuePacket(outapp); safe_delete(outapp); @@ -1208,17 +1215,18 @@ void Client::NukeTraderItem( safe_delete(outapp2); } -void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client *customer, uint16 trader_slot) +void Client::FindAndNukeTraderItem(std::string &item_unique_id, int16 quantity, Client *customer, uint16 trader_slot) { - const EQ::ItemInstance *item = nullptr; - bool stackable = false; - int16 charges = 0; - uint16 slot_id = FindTraderItem(serial_number, quantity); + const EQ::ItemInstance *item = nullptr; + bool stackable = false; + int16 charges = 0; + uint16 slot_id = FindTraderItem(item_unique_id, quantity); + if (slot_id > 0) { item = GetInv().GetItem(slot_id); if (!item) { - LogTrading("Could not find Item: [{}] on Trader: [{}]", serial_number, quantity, GetName()); + LogTrading("Could not find Item: [{}] on Trader: [{}]", item_unique_id, quantity, GetName()); return; } @@ -1236,7 +1244,7 @@ void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client * if (charges <= quantity || (charges <= 0 && quantity == 1) || !stackable) { DeleteItemInInventory(slot_id, quantity); - auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); + auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", CharacterID())); uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ? GetInv().GetLookup()->InventoryTypeSize.Bazaar : trader_items.size(); @@ -1245,7 +1253,7 @@ void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client * std::vector delete_queue{}; for (int i = 0; i < item_limit; i++) { - if (test_slot && trader_items.at(i).item_sn == serial_number) { + if (test_slot && i < trader_items.size() && trader_items.at(i).item_unique_id.compare(item_unique_id) == 0) { delete_queue.push_back(trader_items.at(i)); NukeTraderItem( slot_id, @@ -1253,12 +1261,12 @@ void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client * quantity, customer, trader_slot, - trader_items.at(i).item_sn, + trader_items.at(i).item_unique_id, trader_items.at(i).item_id ); test_slot = false; } - else if (trader_items.at(i).item_id > 0) { + else if (i < trader_items.size() && trader_items.at(i).item_id > 0) { count++; } } @@ -1271,13 +1279,12 @@ void Client::FindAndNukeTraderItem(int32 serial_number, int16 quantity, Client * return; } else { - TraderRepository::UpdateQuantity(database, CharacterID(), item->GetSerialNumber(), charges - quantity); - NukeTraderItem(slot_id, charges, quantity, customer, trader_slot, item->GetSerialNumber(), item->GetID()); + NukeTraderItem(slot_id, charges, quantity, customer, trader_slot, item->GetUniqueID(), item->GetID()); return; } } LogTrading("Could NOT find a match for Item: [{}] with a quantity of: [{}] on Trader: [{}]\n", - serial_number, + item_unique_id, quantity, GetName() ); @@ -1322,235 +1329,250 @@ void Client::TradeRequestFailed(const EQApplicationPacket *app) safe_delete(outapp); } -static void BazaarAuditTrail(const char *seller, const char *buyer, const char *itemName, int quantity, int totalCost, int tranType) { +void Client::TradeRequestFailed(TraderBuy_Struct &in) +{ + auto outapp = EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct)); + auto data = reinterpret_cast(outapp.pBuffer); - 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); + data->method = in.method; + data->action = in.action; + data->sub_action = Failed; + data->already_sold = 0xFFFFFFFF; + data->item_id = in.item_id; + data->price = in.price; + data->quantity = in.quantity; + data->trader_id = 0xFFFFFFFF; + strn0cpy(data->buyer_name, in.buyer_name, sizeof(data->buyer_name)); + strn0cpy(data->item_name, in.item_name, sizeof(data->item_name)); + strn0cpy(data->item_unique_id, in.item_unique_id, sizeof(data->item_unique_id)); + strn0cpy(data->seller_name, in.seller_name, sizeof(data->seller_name)); + + QueuePacket(&outapp); } -void Client::BuyTraderItem(TraderBuy_Struct *tbs, Client *Trader, const EQApplicationPacket *app) +void Client::BuyTraderItem(const EQApplicationPacket *app) { - if (!Trader) { - return; - } + auto in = reinterpret_cast(app->pBuffer); + auto trader = entity_list.GetClientByID(in->trader_id); - if (!Trader->IsTrader()) { + if (!trader || !trader->IsTrader()) { + Message(Chat::Red, "The trader could not be found."); TradeRequestFailed(app); return; } - auto outapp = std::make_unique(OP_Trader, static_cast(sizeof(TraderBuy_Struct))); - auto outtbs = (TraderBuy_Struct *) outapp->pBuffer; - outtbs->item_id = tbs->item_id; + auto trader_packet = std::make_unique(OP_Trader, static_cast(sizeof(TraderBuy_Struct))); + auto data = reinterpret_cast(trader_packet->pBuffer); - const EQ::ItemInstance *buy_item = nullptr; - uint32 item_id = 0; + auto buy_inst = trader->FindTraderItemByUniqueID(in->item_unique_id); + std::unique_ptr inst_copy(buy_inst ? buy_inst->Clone() : nullptr); - if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { - tbs->item_id = Strings::ToUnsignedBigInt(tbs->serial_number); - } - - buy_item = Trader->FindTraderItemBySerialNumber(tbs->item_id); - - if (!buy_item) { - LogTrading("Unable to find item id [{}] item_sn [{}] on trader", tbs->item_id, tbs->serial_number); + if (!buy_inst || !inst_copy) { + LogTrading("Unable to find item id [{}] item_sn [{}] on trader", in->item_id, in->item_unique_id); + Message(Chat::Red, "The trader no longer has the item for sale. Please refresh the merchant window."); TradeRequestFailed(app); return; } + auto quantity = in->quantity; + inst_copy->SetCharges(quantity); + if (buy_inst->GetItem()->MaxCharges > 0) { + inst_copy->SetCharges(buy_inst->GetCharges()); + } + + if (inst_copy->IsStackable() && quantity != buy_inst->GetCharges()) { + auto item_unique_id = database.ReserveNewItemUniqueId(); + if (item_unique_id.empty()) { + Message(Chat::Red, "Internal error. Failed to allocate a unique item id for this purchase."); + TradeRequestFailed(app); + return; + } + + inst_copy->SetUniqueID(item_unique_id); + } + LogTrading( - "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", - buy_item->GetItem()->Name, - buy_item->IsStackable(), - tbs->quantity, - buy_item->GetCharges() + "Name: [{}] IsStackable: [{}] Requested Quantity: [{}]", + buy_inst->GetItem()->Name, + buy_inst->IsStackable(), + quantity ); - // If the item is not stackable, then we can only be buying one of them. - if (!buy_item->IsStackable()) { - outtbs->quantity = 1; // normally you can't send more than 1 here - } - else { - // Stackable items, arrows, diamonds, etc - int32 item_charges = buy_item->GetCharges(); - // ItemCharges for stackables should not be <= 0 - if (item_charges <= 0) { - outtbs->quantity = 1; - // If the purchaser requested more than is in the stack, just sell them how many are actually in the stack. - } - else if (static_cast(item_charges) < tbs->quantity) { - outtbs->quantity = item_charges; - } - else { - outtbs->quantity = tbs->quantity; - } + + if (CheckLoreConflict(inst_copy->GetItem())) { + MessageString(Chat::Red, DUPLICATE_LORE); + TradeRequestFailed(app); + return; } - LogTrading("Actual quantity that will be traded is [{}]", outtbs->quantity); - - if ((tbs->price * outtbs->quantity) <= 0) { + if (in->price * quantity <= 0) { Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1"); - Trader->Message( - Chat::Red, - "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1" - ); + trader->Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1"); LogError( "Bazaar: Zero price transaction between [{}] and [{}] aborted. Item: [{}] Charges: " "[{}] Qty [{}] Price: [{}]", - GetName(), - Trader->GetName(), - buy_item->GetItem()->Name, - buy_item->GetCharges(), - tbs->quantity, - tbs->price + GetCleanName(), + trader->GetCleanName(), + buy_inst->GetItem()->Name, + buy_inst->GetCharges(), + quantity, + in->price ); TradeRequestFailed(app); return; } - uint64 total_transaction_value = static_cast(tbs->price) * static_cast(outtbs->quantity); - - if (total_transaction_value > MAX_TRANSACTION_VALUE) { + uint64 total_transaction_value = static_cast(in->price) * static_cast(quantity); + if (total_transaction_value > EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction) { Message( Chat::Red, "That would exceed the single transaction limit of %u platinum.", - MAX_TRANSACTION_VALUE / 1000 + EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction / 1000 ); TradeRequestFailed(app); return; } - // This cannot overflow assuming MAX_TRANSACTION_VALUE, checked above, is the default of 2000000000 - uint32 total_cost = tbs->price * outtbs->quantity; - - if (Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) { - // RoF+ uses individual item price where older clients use total price - outtbs->price = tbs->price; - } - else { - outtbs->price = total_cost; - } - + uint64 total_cost = in->price * quantity; if (!TakeMoneyFromPP(total_cost)) { - RecordPlayerEventLog( - PlayerEvent::POSSIBLE_HACK, - PlayerEvent::PossibleHackEvent{ - .message = "Attempted to buy something in bazaar but did not have enough money." - } - ); + MessageString(Chat::Red, INSUFFICIENT_FUNDS); TradeRequestFailed(app); return; } - LogTrading("Customer Paid: [{}] in Copper", total_cost); + if (!trader->RemoveItemByItemUniqueId(buy_inst->GetUniqueID(), quantity)) { + AddMoneyToPP(total_cost, true); + Message(Chat::Red, "The Trader no longer has the item. Please refresh the merchant window."); + TradeRequestFailed(app); + return; + } - uint32 platinum = total_cost / 1000; - total_cost -= (platinum * 1000); - uint32 gold = total_cost / 100; - total_cost -= (gold * 100); - uint32 silver = total_cost / 10; - total_cost -= (silver * 10); - uint32 copper = total_cost; + trader->AddMoneyToPP(total_cost, true); - Trader->AddMoneyToPP(copper, silver, gold, platinum, true); + if (!PutItemInInventoryWithStacking(inst_copy.get())) { + AddMoneyToPP(total_cost, true); + trader->TakeMoneyFromPP(total_cost, true); + trader->PutItemInInventoryWithStacking(buy_inst); + MessageString(Chat::Red, HOW_CAN_YOU_BUY_MORE, trader->GetCleanName()); + TradeRequestFailed(app); + return; + } - if (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { + auto [slot_id, merchant_data] = GetDataFromMerchantListByItemUniqueId(buy_inst->GetUniqueID()); + auto [item_id, merchant_quantity, item_unique_id] = merchant_data; + + data->action = BazaarBuyItem; + data->price = in->price; + data->quantity = quantity; + data->trader_id = trader->GetID(); + strn0cpy(data->seller_name, trader->GetCleanName(), sizeof(data->seller_name)); + strn0cpy(data->buyer_name, GetCleanName(), sizeof(data->buyer_name)); + strn0cpy(data->item_name, buy_inst->GetItem()->Name, sizeof(data->item_name)); + strn0cpy(data->item_unique_id, buy_inst->GetUniqueID().data(), sizeof(data->item_unique_id)); + trader->QueuePacket(trader_packet.get()); + + QueuePacket(app); + + LogTrading("Customer Paid: [{}] to {}", DetermineMoneyString(total_cost), trader->GetCleanName()); + LogTrading("Customer Received: [{}] {} {} with unique_id of {}", + quantity, + in->item_name, + inst_copy->GetItem()->MaxCharges > 0 ? fmt::format("with {} charges ", inst_copy->GetCharges()).c_str() : std::string(""), + inst_copy->GetUniqueID() + ); + LogTrading("Trader Received: [{}] from {}", DetermineMoneyString(total_cost), GetCleanName()); + LogTrading("Trader Sent: [{}] {} {} with unique_id of {}", + quantity, + in->item_name, + buy_inst->GetItem()->MaxCharges > 0 ? fmt::format("with {} charges ", buy_inst->GetCharges()).c_str() : std::string(""), + buy_inst->GetUniqueID() + ); + + if (merchant_quantity > quantity) { + std::unique_ptr vendor_inst(buy_inst ? buy_inst->Clone() : nullptr); + vendor_inst->SetMerchantCount(merchant_quantity - quantity); + vendor_inst->SetMerchantSlot(slot_id); + vendor_inst->SetPrice(in->price); + + auto list = GetTraderMerchantList(); + std::get<1>(list->at(slot_id)) -= quantity; + + TraderRepository::UpdateQuantity(database, item_unique_id, merchant_quantity - quantity); + SendItemPacket(slot_id, vendor_inst.get(), ItemPacketMerchant); + } + else { + auto client_packet = new EQApplicationPacket(OP_ShopDelItem, static_cast(sizeof(Merchant_DelItem_Struct))); + auto client_data = reinterpret_cast(client_packet->pBuffer); + + client_data->npcid = trader->GetID(); + client_data->playerid = GetID(); + client_data->itemslot = slot_id; + + QueuePacket(client_packet); + safe_delete(client_packet); + + auto list = GetTraderMerchantList(); + list->erase(slot_id); + + TraderRepository::DeleteWhere(database, fmt::format("`item_unique_id` = '{}'", item_unique_id)); + } + + if (buy_inst && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { auto e = PlayerEvent::TraderPurchaseEvent{ - .item_id = buy_item->GetID(), - .augment_1_id = buy_item->GetAugmentItemID(0), - .augment_2_id = buy_item->GetAugmentItemID(1), - .augment_3_id = buy_item->GetAugmentItemID(2), - .augment_4_id = buy_item->GetAugmentItemID(3), - .augment_5_id = buy_item->GetAugmentItemID(4), - .augment_6_id = buy_item->GetAugmentItemID(5), - .item_name = buy_item->GetItem()->Name, - .trader_id = Trader->CharacterID(), - .trader_name = Trader->GetCleanName(), - .price = tbs->price, - .quantity = outtbs->quantity, - .charges = buy_item->GetCharges(), - .total_cost = (tbs->price * outtbs->quantity), + .item_id = buy_inst->GetID(), + .augment_1_id = buy_inst->GetAugmentItemID(0), + .augment_2_id = buy_inst->GetAugmentItemID(1), + .augment_3_id = buy_inst->GetAugmentItemID(2), + .augment_4_id = buy_inst->GetAugmentItemID(3), + .augment_5_id = buy_inst->GetAugmentItemID(4), + .augment_6_id = buy_inst->GetAugmentItemID(5), + .item_name = buy_inst->GetItem()->Name, + .trader_id = trader->CharacterID(), + .trader_name = trader->GetCleanName(), + .price = in->price, + .quantity = quantity, + .charges = buy_inst->GetCharges(), + .total_cost = total_cost, .player_money_balance = GetCarriedMoney(), + .offline_purchase = trader->IsOffline(), }; RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); } - if (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) { + if (buy_inst && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) { auto e = PlayerEvent::TraderSellEvent{ - .item_id = buy_item->GetID(), - .augment_1_id = buy_item->GetAugmentItemID(0), - .augment_2_id = buy_item->GetAugmentItemID(1), - .augment_3_id = buy_item->GetAugmentItemID(2), - .augment_4_id = buy_item->GetAugmentItemID(3), - .augment_5_id = buy_item->GetAugmentItemID(4), - .augment_6_id = buy_item->GetAugmentItemID(5), - .item_name = buy_item->GetItem()->Name, + .item_id = buy_inst->GetID(), + .augment_1_id = buy_inst->GetAugmentItemID(0), + .augment_2_id = buy_inst->GetAugmentItemID(1), + .augment_3_id = buy_inst->GetAugmentItemID(2), + .augment_4_id = buy_inst->GetAugmentItemID(3), + .augment_5_id = buy_inst->GetAugmentItemID(4), + .augment_6_id = buy_inst->GetAugmentItemID(5), + .item_name = buy_inst->GetItem()->Name, .buyer_id = CharacterID(), .buyer_name = GetCleanName(), - .price = tbs->price, - .quantity = outtbs->quantity, - .charges = buy_item->GetCharges(), - .total_cost = (tbs->price * outtbs->quantity), - .player_money_balance = Trader->GetCarriedMoney(), + .price = in->price, + .quantity = quantity, + .charges = buy_inst->GetCharges(), + .total_cost = total_cost, + .player_money_balance = trader->GetCarriedMoney(), + .offline_purchase = trader->IsOffline(), }; - RecordPlayerEventLogWithClient(Trader, PlayerEvent::TRADER_SELL, e); + 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); + } } - - LogTrading("Trader Received: [{}] Platinum, [{}] Gold, [{}] Silver, [{}] Copper", platinum, gold, silver, copper); - ReturnTraderReq(app, outtbs->quantity, item_id); - - outtbs->trader_id = GetID(); - outtbs->action = BazaarBuyItem; - strn0cpy(outtbs->seller_name, Trader->GetCleanName(), sizeof(outtbs->seller_name)); - strn0cpy(outtbs->buyer_name, GetCleanName(), sizeof(outtbs->buyer_name)); - strn0cpy(outtbs->item_name, buy_item->GetItem()->Name, sizeof(outtbs->item_name)); - strn0cpy( - outtbs->serial_number, - fmt::format("{:016}", buy_item->GetSerialNumber()).c_str(), - sizeof(outtbs->serial_number) - ); - - TraderRepository::Trader t{}; - t.item_charges = buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges(); - t.item_id = buy_item->GetItem()->ID; - t.aug_slot_1 = buy_item->GetAugmentItemID(0); - t.aug_slot_2 = buy_item->GetAugmentItemID(1); - t.aug_slot_3 = buy_item->GetAugmentItemID(2); - t.aug_slot_4 = buy_item->GetAugmentItemID(3); - t.aug_slot_5 = buy_item->GetAugmentItemID(4); - t.aug_slot_6 = buy_item->GetAugmentItemID(5); - t.char_id = CharacterID(); - t.slot_id = FindNextFreeParcelSlot(CharacterID()); - - SendTraderItem( - buy_item->GetItem()->ID, - buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges(), - t - ); - - if (RuleB(Bazaar, AuditTrail)) { - BazaarAuditTrail(Trader->GetName(), GetName(), buy_item->GetItem()->Name, outtbs->quantity, outtbs->price, 0); - } - - Trader->FindAndNukeTraderItem(tbs->item_id, outtbs->quantity, this, 0); - - if (item_id > 0 && Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) { - // Convert Serial Number back to ItemID for RoF+ - outtbs->item_id = item_id; - } - - Trader->QueuePacket(outapp.get()); } void Client::SendBazaarWelcome() @@ -1630,12 +1652,12 @@ static void UpdateTraderCustomerItemsAdded( database.CreateItem( i.item_id, i.item_charges, - i.aug_slot_1, - i.aug_slot_2, - i.aug_slot_3, - i.aug_slot_4, - i.aug_slot_5, - i.aug_slot_6 + i.augment_one, + i.augment_two, + i.augment_three, + i.augment_four, + i.augment_five, + i.augment_six ) ); if (!inst) { @@ -1644,15 +1666,14 @@ static void UpdateTraderCustomerItemsAdded( inst->SetCharges(i.item_charges); inst->SetPrice(i.item_cost); - inst->SetSerialNumber(i.item_sn); - inst->SetMerchantSlot(i.item_sn); + inst->SetUniqueID(i.item_unique_id); if (inst->IsStackable()) { inst->SetMerchantCount(i.item_charges); } customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor? LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges", - item->Name, i.item_sn, i.item_charges); + item->Name, i.item_unique_id, i.item_charges); } } } @@ -1696,12 +1717,8 @@ static void UpdateTraderCustomerPriceChanged( // RoF+ use Item IDs for now tdis->item_id = trader_items.at(i).item_id; } - else { - tdis->item_id = trader_items.at(i).item_sn; - } - tdis->item_id = trader_items.at(i).item_sn; LogTrading("Telling customer to remove item [{}] with [{}] charges and S/N [{}]", - item_id, charges, trader_items.at(i).item_sn); + item_id, charges, trader_items.at(i).item_unique_id); customer->QueuePacket(outapp); } @@ -1713,17 +1730,23 @@ static void UpdateTraderCustomerPriceChanged( LogTrading("Sending price updates to customer [{}]", customer->GetName()); - auto it = std::find_if(trader_items.begin(), trader_items.end(), [&](TraderRepository::Trader x){ return x.item_id == item->ID;}); + auto it = std::find_if( + trader_items.begin(), + trader_items.end(), + [&](TraderRepository::Trader x) { + return x.item_id == item->ID; + } + ); std::unique_ptr inst( database.CreateItem( it->item_id, it->item_charges, - it->aug_slot_1, - it->aug_slot_2, - it->aug_slot_3, - it->aug_slot_4, - it->aug_slot_5, - it->aug_slot_6 + it->augment_one, + it->augment_two, + it->augment_three, + it->augment_four, + it->augment_five, + it->augment_six ) ); if (!inst) { @@ -1748,15 +1771,13 @@ static void UpdateTraderCustomerPriceChanged( continue; } - inst->SetSerialNumber(trader_items.at(i).item_sn); - inst->SetMerchantSlot(trader_items.at(i).item_sn); + inst->SetUniqueID(trader_items.at(i).item_unique_id); LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges", - item->Name, trader_items.at(i).item_sn, trader_items.at(i).item_charges); + item->Name, trader_items.at(i).item_unique_id, trader_items.at(i).item_charges); customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor?? } -// safe_delete(inst); } void Client::SendBuyerResults(BarterSearchRequest_Struct& bsr) @@ -1906,12 +1927,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)); @@ -2012,6 +2039,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, @@ -2154,6 +2193,7 @@ void Client::ToggleBuyerMode(bool status) SetCustomerID(0); SendBuyerMode(true); SendBuyerToBarterWindow(this, Barter_AddToBarterWindow); + UpdateWho(); Message(Chat::Yellow, "Barter Mode ON."); } else { @@ -2166,6 +2206,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()); } @@ -2452,239 +2494,124 @@ void Client::SendTraderMode(BazaarTraderBarterActions status) safe_delete(outapp); } -void Client::TraderPriceUpdate(const EQApplicationPacket *app) +void Client::TraderUpdateItem(const EQApplicationPacket *app) { - // Handle price updates from the Trader and update a customer browsing our stuff if necessary - // This method also handles removing items from sale and adding them back up whilst still in - // Trader mode. - // - auto tpus = (TraderPriceUpdate_Struct *) app->pBuffer; + auto in = reinterpret_cast(app->pBuffer); + uint32 new_price = in->new_price; + auto inst = FindTraderItemByUniqueID(in->item_unique_id); + auto customer = entity_list.GetClientByID(GetCustomerID()); - LogTrading( - "Received Price Update for [{}] Item Serial No. [{}] New Price [{}]", - GetName(), - tpus->SerialNumber, - tpus->NewPrice - ); - - // Pull the items this Trader currently has for sale from the trader table. - // - auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID())); - uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar - ? GetInv().GetLookup()->InventoryTypeSize.Bazaar - : trader_items.size(); - - // The client only sends a single update with the Serial Number of the item whose price has been updated. - // We must update the price for all the Trader's items that are identical to that one item, i.e. - // if it is a stackable item like arrows, update the price for all stacks. If it is not stackable, then - // update the prices for all items that have the same number of charges. - // - uint32 id_of_item_to_update = 0; - int32 charges_on_item_to_update = 0; - uint32 old_price = 0; - - for (int i = 0; i < item_limit; i++) { - if ((trader_items.at(i).item_id > 0) && (trader_items.at(i).item_sn == tpus->SerialNumber)) { - // We found the item that the Trader wants to change the price of (or add back up for sale). - // - id_of_item_to_update = trader_items.at(i).item_id; - charges_on_item_to_update = trader_items.at(i).item_charges; - old_price = trader_items.at(i).item_cost; - - LogTrading( - "ItemID is [{}] Charges is [{}]", - trader_items.at(i).item_id, - trader_items.at(i).item_charges - ); - break; - } - } - - if (id_of_item_to_update == 0) { - // If the item is not currently in the trader table for this Trader, then they must have removed it from sale while - // still in Trader mode. Check if the item is in their Trader Satchels, and if so, put it back up. - // Quick Sanity check. If the item is not currently up for sale, and the new price is zero, just ack the packet - // and do nothing. - if (tpus->NewPrice == 0) { - tpus->SubAction = BazaarPriceChange_RemoveItem; - QueuePacket(app); + if (new_price == 0) { + auto result = TraderRepository::DeleteWhere(database, fmt::format("`item_unique_id` = '{}'", in->item_unique_id)); + if (!result) { + LogError("Trader {} attempt to remove item_unique_id {} failed", CharacterID(), in->item_unique_id); return; } - LogTrading("Unable to find item to update price for. Rechecking trader satchels"); - - // Find what is in their Trader Satchels - auto newgis = GetTraderItems(); - uint32 id_of_item_to_add = 0; - int32 charges_on_item_to_add = 0; - - for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { - if ((newgis->items[i] > 0) && (newgis->serial_number[i] == tpus->SerialNumber)) { - id_of_item_to_add = newgis->items[i]; - charges_on_item_to_add = newgis->charges[i]; - - LogTrading( - "Found new Item to Add, ItemID is [{}] Charges is [{}]", - newgis->items[i], - newgis->charges[i] - ); - break; - } - } - - const EQ::ItemData *item = nullptr; - if (id_of_item_to_add) { - item = database.GetItem(id_of_item_to_add); - } - - if (!id_of_item_to_add || !item) { - tpus->SubAction = BazaarPriceChange_Fail; - QueuePacket(app); - TraderEndTrader(); - safe_delete(newgis); - - LogTrading("Item not found in Trader Satchels either"); - return; - } - - // It is a limitation of the client that if you have multiple of the same item, but with different charges, - // although you can set different prices for them before entering Trader mode. If you Remove them and then - // add them back whilst still in Trader mode, they all go up for the same price. We check for this situation - // and give the Trader a warning message. - // - if (!item->Stackable) { - bool same_item_with_differing_charges = false; - - for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { - if ((newgis->items[i] == id_of_item_to_add) && (newgis->charges[i] != charges_on_item_to_add)) { - same_item_with_differing_charges = true; - break; - } - } - - if (same_item_with_differing_charges) { - Message( - Chat::Red, - "Warning: You have more than one %s with different charges. They have all been added for sale " - "at the same price.", - item->Name - ); - } - } - - // Now put all Items with a matching ItemID up for trade. - // - for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) { - if (newgis->items[i] == id_of_item_to_add) { - auto item_detail = FindTraderItemBySerialNumber(newgis->serial_number[i]); - - TraderRepository::Trader trader_item{}; - trader_item.id = 0; - trader_item.char_entity_id = GetID(); - trader_item.char_id = CharacterID(); - trader_item.char_zone_id = GetZoneID(); - trader_item.char_zone_instance_id = GetInstanceID(); - trader_item.item_charges = newgis->charges[i]; - trader_item.item_cost = tpus->NewPrice; - trader_item.item_id = newgis->items[i]; - trader_item.item_sn = newgis->serial_number[i]; - trader_item.listing_date = time(nullptr); - if (item_detail->IsAugmented()) { - auto augs = item_detail->GetAugmentIDs(); - trader_item.aug_slot_1 = augs.at(0); - trader_item.aug_slot_2 = augs.at(1); - trader_item.aug_slot_3 = augs.at(2); - trader_item.aug_slot_4 = augs.at(3); - trader_item.aug_slot_5 = augs.at(4); - trader_item.aug_slot_6 = augs.at(5); - } - trader_item.slot_id = i; - - TraderRepository::ReplaceOne(database, trader_item); - - trader_items.push_back(trader_item); - - LogTrading( - "Adding new item for [{}] ItemID [{}] SerialNumber [{}] Charges [{}] " - "Price: [{}] Slot [{}]", - GetName(), - newgis->items[i], - newgis->serial_number[i], - newgis->charges[i], - tpus->NewPrice, - i - ); - } - } - - // If we have a customer currently browsing, update them with the new items. - // - if (GetCustomerID()) { - UpdateTraderCustomerItemsAdded( - GetCustomerID(), - trader_items, - id_of_item_to_add, - GetInv().GetLookup()->InventoryTypeSize.Bazaar - ); - } - - safe_delete(newgis); - - // Acknowledge to the client. - tpus->SubAction = BazaarPriceChange_AddItem; + in->sub_action = BazaarPriceChange_RemoveItem; QueuePacket(app); + if (customer && inst) { + auto list = customer->GetTraderMerchantList(); + auto client_packet = + new EQApplicationPacket(OP_ShopDelItem, static_cast(sizeof(Merchant_DelItem_Struct))); + + auto client_data = reinterpret_cast(client_packet->pBuffer); + client_data->npcid = GetID(); + client_data->playerid = customer->GetID(); + + for (auto const [slot_id, merchant_data]: *list) { + auto const [item_id, merchant_quantity, item_unique_id] = merchant_data; + if (item_id == inst->GetID()) { + client_data->itemslot = slot_id; + customer->QueuePacket(client_packet); + AddDataToMerchantList(slot_id, 0, 0, "0000000000000000"); + } + } + safe_delete(client_packet); + + customer->Message( + Chat::Red, + fmt::format( + "Trader {} removed item {} from the bazaar. Item no longer available.", + GetCleanName(), + inst->GetItem()->Name + ).c_str() + ); + LogTrading("Trader removed item from trader list with item_unique_id {}", in->item_unique_id); + } + return; } - // This is a safeguard against a Trader increasing the price of an item while a customer is browsing and - // unwittingly buying it at a higher price than they were expecting to. - // - if ((old_price != 0) && (tpus->NewPrice > old_price) && GetCustomerID()) { - tpus->SubAction = BazaarPriceChange_Fail; - QueuePacket(app); - TraderEndTrader(); - Message( - Chat::Red, - "You must remove the item from sale before you can increase the price while a customer is browsing." - ); - Message(Chat::Red, "Click 'Begin Trader' to restart Trader mode with the increased price for this item."); - return; - } + auto result = TraderRepository::UpdatePrice(database, in->item_unique_id, new_price); + if (result.empty()) { + auto trader_items = FindTraderItemsByUniqueID(in->item_unique_id); + std::vector queue{}; + for (auto const& i : trader_items) { + TraderRepository::Trader e{}; - // Send Acknowledgement back to the client. - if (old_price == 0) { - tpus->SubAction = BazaarPriceChange_AddItem; - } - else if (tpus->NewPrice != 0) { - tpus->SubAction = BazaarPriceChange_UpdatePrice; + e.id = 0; + e.char_entity_id = GetID(); + e.character_id = CharacterID(); + e.char_zone_id = GetZoneID(); + e.char_zone_instance_id = GetInstanceID(); + e.item_charges = i->GetCharges(); + e.item_cost = new_price; + e.item_id = i->GetID(); + e.item_unique_id = i->GetUniqueID(); + e.slot_id = 0; + e.listing_date = time(nullptr); + if (i->IsAugmented()) { + auto augs = i->GetAugmentIDs(); + e.augment_one = augs.at(0); + e.augment_two = augs.at(1); + e.augment_three = augs.at(2); + e.augment_four = augs.at(3); + e.augment_five = augs.at(4); + e.augment_six = augs.at(5); + } + + queue.push_back(e); + if (customer) { + int16 next_slot_id = GetNextFreeSlotFromMerchantList(); + if (next_slot_id != INVALID_INDEX) { + std::unique_ptr vendor_inst_copy(i ? i->Clone() : nullptr); + vendor_inst_copy->SetUniqueID(i->GetUniqueID()); + vendor_inst_copy->SetMerchantCount(i->IsStackable() ? i->GetCharges() : 1); + vendor_inst_copy->SetMerchantSlot(next_slot_id ); + vendor_inst_copy->SetPrice(new_price); + AddDataToMerchantList(next_slot_id, i->GetID(), i->GetMerchantCount(), i->GetUniqueID()); + customer->SendItemPacket(next_slot_id, vendor_inst_copy.get(), ItemPacketMerchant); + } + } + } + + if (!queue.empty()) { + TraderRepository::ReplaceMany(database, queue); + } } else { - tpus->SubAction = BazaarPriceChange_RemoveItem; - } - - QueuePacket(app); - - if (old_price == tpus->NewPrice) { - LogTrading("The new price is the same as the old one"); - return; - } - // Update the price for all items we have for sale that have this ItemID and number of charges, or remove - // them from the trader table if the new price is zero. - // - database.UpdateTraderItemPrice(CharacterID(), id_of_item_to_update, charges_on_item_to_update, tpus->NewPrice); - - // If a customer is browsing our goods, send them the updated prices / remove the items from the Merchant window - if (GetCustomerID()) { - UpdateTraderCustomerPriceChanged( - GetCustomerID(), - trader_items, - id_of_item_to_update, - charges_on_item_to_update, - tpus->NewPrice, - item_limit + if (customer) { + for (auto const i : result) { + auto [slot_id, merchant_data] = customer->GetDataFromMerchantListByItemUniqueId(i.item_unique_id); + auto [item_id, merchant_quantity, item_unique_id] = merchant_data; + std::unique_ptr vendor_inst_copy(inst ? inst->Clone() : nullptr); + vendor_inst_copy->SetUniqueID(i.item_unique_id); + vendor_inst_copy->SetMerchantCount(i.item_charges); + vendor_inst_copy->SetMerchantSlot(slot_id); + vendor_inst_copy->SetPrice(new_price); + customer->SendItemPacket(slot_id, vendor_inst_copy.get(), ItemPacketMerchant); + } + customer->Message( + Chat::Red, + fmt::format("Trader {} updated the price of item {}", GetCleanName(), inst->GetItem()->Name).c_str() ); + } } + + in->sub_action = BazaarPriceChange_UpdatePrice; + QueuePacket(app); } void Client::SendBazaarDone(uint32 trader_id) @@ -2768,32 +2695,12 @@ void Client::SendBulkBazaarTraders() void Client::DoBazaarInspect(BazaarInspect_Struct &in) { - if (RuleB(Bazaar, UseAlternateBazaarSearch)) { - if (in.trader_id >= TraderRepository::TRADER_CONVERT_ID) { - auto trader = TraderRepository::GetTraderByInstanceAndSerialnumber( - database, - in.trader_id - TraderRepository::TRADER_CONVERT_ID, - fmt::format("{}", in.serial_number).c_str() - ); - - if (!trader.trader_id) { - LogTrading("Unable to convert trader id for {} and serial number {}. Trader Buy aborted.", - in.trader_id - TraderRepository::TRADER_CONVERT_ID, - in.serial_number - ); - return; - } - - in.trader_id = trader.trader_id; - } - } - auto items = TraderRepository::GetWhere( - database, fmt::format("`char_id` = '{}' AND `item_sn` = '{}'", in.trader_id, in.serial_number) + database, fmt::format("`item_unique_id` = '{}'", in.item_unique_id) ); if (items.empty()) { - LogInfo("Failed to find item with serial number [{}]", in.serial_number); + LogInfo("Failed to find item with serial number [{}]", in.item_unique_id); return; } @@ -2803,12 +2710,12 @@ void Client::DoBazaarInspect(BazaarInspect_Struct &in) database.CreateItem( item.item_id, item.item_charges, - item.aug_slot_1, - item.aug_slot_2, - item.aug_slot_3, - item.aug_slot_4, - item.aug_slot_5, - item.aug_slot_6 + item.augment_one, + item.augment_two, + item.augment_three, + item.augment_four, + item.augment_five, + item.augment_six ) ); @@ -2860,15 +2767,52 @@ std::string Client::DetermineMoneyString(uint64 cp) return fmt::format("{}", money); } -void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicationPacket *app) +void Client::BuyTraderItemFromBazaarWindow(const EQApplicationPacket *app) { - auto in = (TraderBuy_Struct *) app->pBuffer; - auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number, tbs->trader_id); + auto in = reinterpret_cast(app->pBuffer); + auto trader_item = TraderRepository::GetItemByItemUniqueNumber(database, in->item_unique_id); + + LogTradingDetail( + "Packet details: \n" + "Action :{}\n" + "Method :{}\n" + "SubAction :{}\n" + "Unknown_012 :{}\n" + "Trader ID :{}\n" + "Buyer Name :{}\n" + "Seller Name :{}\n" + "Unknown_148 :{}\n" + "Item Name :{}\n" + "Item Unique ID :{}\n" + "Unknown_261 :{}\n" + "Item ID :{}\n" + "Price :{}\n" + "Already Sold :{}\n" + "Unknown_276 :{}\n" + "Quantity :{}\n", + in->action, + in->method, + in->sub_action, + in->unknown_012, + in->trader_id, + in->buyer_name, + in->seller_name, + in->unknown_148, + in->item_name, + in->item_unique_id, + in->unknown_261, + in->item_id, + in->price, + in->already_sold, + in->unknown_276, + in->quantity + ); + if (!trader_item.id || GetTraderTransactionDate() < trader_item.listing_date) { - LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " - "[{}] The Traders data was outdated.", - tbs->trader_id, - tbs->serial_number + LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item unique_id " + "[{}] The Traders data was outdated.", + in->trader_id, + in->item_unique_id ); in->method = BazaarByParcel; in->sub_action = DataOutDated; @@ -2877,10 +2821,10 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati } if (trader_item.active_transaction) { - LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " - "[{}] The item is already within an active transaction.", - tbs->trader_id, - tbs->serial_number + LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " + "[{}] The item is already within an active transaction.", + in->trader_id, + in->item_unique_id ); in->method = BazaarByParcel; in->sub_action = DataOutDated; @@ -2888,40 +2832,13 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati return; } - TraderRepository::UpdateActiveTransaction(database, trader_item.id, true); - - std::unique_ptr buy_item( - database.CreateItem( - trader_item.item_id, - trader_item.item_charges, - trader_item.aug_slot_1, - trader_item.aug_slot_2, - trader_item.aug_slot_3, - trader_item.aug_slot_4, - trader_item.aug_slot_5, - trader_item.aug_slot_6 - ) - ); - - if (!buy_item) { - LogTrading("Unable to find item id [{}] item_sn [{}] on trader", - trader_item.item_id, - trader_item.item_sn - ); - in->method = BazaarByParcel; - in->sub_action = Failed; - TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); - TradeRequestFailed(app); - return; - } - auto next_slot = FindNextFreeParcelSlot(CharacterID()); if (next_slot == INVALID_INDEX) { LogTrading( - "{} attempted to purchase {} from the bazaar with parcel delivery. Unfortunately their parcel limit was reached. " - "Purchase unsuccessful.", + "{} attempted to purchase {} from the bazaar with parcel delivery. Unfortunately their parcel limit was " + "reached. Purchase unsuccessful.", GetCleanName(), - buy_item->GetItem()->Name + in->item_name ); in->method = BazaarByParcel; in->sub_action = TooManyParcels; @@ -2930,55 +2847,39 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati return; } - LogTrading( - "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", - buy_item->GetItem()->Name, - buy_item->IsStackable(), - tbs->quantity, - buy_item->GetCharges() - ); + TraderRepository::UpdateActiveTransaction(database, trader_item.id, true); - // Determine the actual quantity for the purchase - int32 charges = static_cast(tbs->quantity); - if (!buy_item->IsStackable()) { - if (buy_item->GetCharges() <= 0) { - charges = 1; - } - else { - charges = buy_item->GetCharges(); - } + uint32 quantity = in->quantity; + auto item = database.GetItem(trader_item.item_id); + + int16 charges = 1; + if (trader_item.item_charges > 0 || item->Stackable || item->MaxCharges > 0) { + charges = trader_item.item_charges; } - LogTrading( - "Actual quantity that will be traded is [{}] {}", - tbs->quantity, - buy_item->GetCharges() ? fmt::format("with {} charges", buy_item->GetCharges()) : "" + LogTradingDetail( + "Step 1:Bazaar Purchase. Buyer [{}] Seller [{}] Quantity [{}] Charges [{}] Item_Unique_ID [{}]", + CharacterID(), + in->trader_id, + quantity, + charges, + in->item_unique_id ); - uint64 total_cost = static_cast(tbs->price) * static_cast(tbs->quantity); - if (total_cost > MAX_TRANSACTION_VALUE) { + uint64 total_cost = static_cast(in->price) * static_cast(quantity); + if (total_cost > EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction) { Message( Chat::Red, "That would exceed the single transaction limit of %u platinum.", - MAX_TRANSACTION_VALUE / 1000 + EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction / 1000 ); TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } - uint64 fee = std::round(total_cost * RuleR(Bazaar, ParcelDeliveryCostMod)); + uint64 fee = std::round(total_cost * RuleR(Bazaar, ParcelDeliveryCostMod)); if (!TakeMoneyFromPP(total_cost + fee, false)) { - RecordPlayerEventLog( - PlayerEvent::POSSIBLE_HACK, - PlayerEvent::PossibleHackEvent{ - .message = fmt::format( - "{} attempted to buy {} in bazaar but did not have enough money.", - GetCleanName(), - buy_item->GetItem()->Name - ) - } - ); in->method = BazaarByParcel; in->sub_action = InsufficientFunds; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); @@ -2987,122 +2888,100 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati } Message(Chat::Red, fmt::format("You paid {} for the parcel delivery.", DetermineMoneyString(fee)).c_str()); - LogTrading("Customer [{}] Paid: [{}] in Copper", CharacterID(), total_cost); + SendMoneyUpdate(); - if (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { - auto e = PlayerEvent::TraderPurchaseEvent{ - .item_id = buy_item->GetID(), - .augment_1_id = buy_item->GetAugmentItemID(0), - .augment_2_id = buy_item->GetAugmentItemID(1), - .augment_3_id = buy_item->GetAugmentItemID(2), - .augment_4_id = buy_item->GetAugmentItemID(3), - .augment_5_id = buy_item->GetAugmentItemID(4), - .augment_6_id = buy_item->GetAugmentItemID(5), - .item_name = buy_item->GetItem()->Name, - .trader_id = tbs->trader_id, - .trader_name = tbs->seller_name, - .price = tbs->price, - .quantity = tbs->quantity, - .charges = buy_item->IsStackable() ? 1 : charges, - .total_cost = total_cost, - .player_money_balance = GetCarriedMoney(), - }; - - RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); - } - - CharacterParcelsRepository::CharacterParcels parcel_out{}; - parcel_out.from_name = tbs->seller_name; - parcel_out.note = "Delivered from a Bazaar Purchase"; - parcel_out.sent_date = time(nullptr); - parcel_out.quantity = charges; - parcel_out.item_id = buy_item->GetItem()->ID; - parcel_out.aug_slot_1 = buy_item->GetAugmentItemID(0); - parcel_out.aug_slot_2 = buy_item->GetAugmentItemID(1); - parcel_out.aug_slot_3 = buy_item->GetAugmentItemID(2); - parcel_out.aug_slot_4 = buy_item->GetAugmentItemID(3); - parcel_out.aug_slot_5 = buy_item->GetAugmentItemID(4); - parcel_out.aug_slot_6 = buy_item->GetAugmentItemID(5); - parcel_out.char_id = CharacterID(); - parcel_out.slot_id = next_slot; - parcel_out.id = 0; - - auto result = CharacterParcelsRepository::InsertOne(database, parcel_out); - if (!result.id) { - LogError("Failed to add parcel to database. From {} to {} item {} quantity {}", - parcel_out.from_name, - GetCleanName(), - parcel_out.item_id, - parcel_out.quantity - ); - Message(Chat::Yellow, "Unable to save parcel to the database. Please contact an administrator."); - in->method = BazaarByParcel; - in->sub_action = Failed; - TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); - TradeRequestFailed(app); - return; - } - - ReturnTraderReq(app, tbs->quantity, buy_item->GetID()); - if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::PARCEL_SEND)) { - PlayerEvent::ParcelSend e{}; - e.from_player_name = parcel_out.from_name; - e.to_player_name = GetCleanName(); - e.item_id = parcel_out.item_id; - e.augment_1_id = parcel_out.aug_slot_1; - e.augment_2_id = parcel_out.aug_slot_2; - e.augment_3_id = parcel_out.aug_slot_3; - e.augment_4_id = parcel_out.aug_slot_4; - e.augment_5_id = parcel_out.aug_slot_5; - e.augment_6_id = parcel_out.aug_slot_6; - e.quantity = tbs->quantity; - e.charges = buy_item->IsStackable() ? 1 : charges; - e.sent_date = parcel_out.sent_date; - - RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e); - } - - Parcel_Struct ps{}; - ps.item_slot = parcel_out.slot_id; - strn0cpy(ps.send_to, GetCleanName(), sizeof(ps.send_to)); - - if (trader_item.item_charges <= static_cast(tbs->quantity) || !buy_item->IsStackable()) { - TraderRepository::DeleteOne(database, trader_item.id); - } else { - TraderRepository::UpdateQuantity( - database, - trader_item.char_id, - trader_item.item_sn, - trader_item.item_charges - tbs->quantity - ); - } - - SendParcelDeliveryToWorld(ps); - - if (RuleB(Bazaar, AuditTrail)) { - BazaarAuditTrail(tbs->seller_name, GetName(), buy_item->GetItem()->Name, tbs->quantity, tbs->price, 0); - } - - auto out_server = std::make_unique( - ServerOP_BazaarPurchase, static_cast(sizeof(BazaarPurchaseMessaging_Struct)) + LogTradingDetail("Step 2:Bazaar Purchase. Took [{}] {}from Buyer [{}] for purchase of [{}] {}{}", + DetermineMoneyString(total_cost), + fee > 0 ? fmt::format("plus a fee of [{}] ", fee) : std::string(""), + CharacterID(), + quantity, + quantity > 1 ? fmt::format("{}s", in->item_name) : in->item_name, + item->MaxCharges > 0 ? fmt::format(" with charges of [{}]", charges) : std::string("") ); - auto out_data = (BazaarPurchaseMessaging_Struct *) out_server->pBuffer; - out_data->trader_buy_struct = *tbs; - out_data->buyer_id = CharacterID(); - out_data->item_aug_1 = buy_item->GetAugmentItemID(0); - out_data->item_aug_2 = buy_item->GetAugmentItemID(1); - out_data->item_aug_3 = buy_item->GetAugmentItemID(2); - out_data->item_aug_4 = buy_item->GetAugmentItemID(3); - out_data->item_aug_5 = buy_item->GetAugmentItemID(4); - out_data->item_aug_6 = buy_item->GetAugmentItemID(5); - out_data->item_quantity_available = trader_item.item_charges; - out_data->id = trader_item.id; + auto out_server = std::make_unique(ServerOP_BazaarPurchase, sizeof(BazaarPurchaseMessaging_Struct)); + auto out_data = reinterpret_cast(out_server->pBuffer); + + out_data->transaction_status = BazaarPurchaseBuyerCompleteSendToSeller; + out_data->trader_buy_struct.action = in->action; + out_data->trader_buy_struct.method = in->method; + out_data->trader_buy_struct.already_sold = in->already_sold; + out_data->trader_buy_struct.item_id = item->ID; + out_data->trader_buy_struct.price = in->price; + out_data->trader_buy_struct.quantity = in->quantity; + out_data->trader_buy_struct.sub_action = in->sub_action; + out_data->trader_buy_struct.trader_id = trader_item.character_id; + out_data->buyer_id = CharacterID(); + out_data->item_aug_1 = trader_item.augment_one; + out_data->item_aug_2 = trader_item.augment_two; + out_data->item_aug_3 = trader_item.augment_three; + out_data->item_aug_4 = trader_item.augment_four; + out_data->item_aug_5 = trader_item.augment_five; + out_data->item_aug_6 = trader_item.augment_six; + out_data->item_quantity = quantity; + out_data->item_charges = charges; + out_data->id = trader_item.id; + out_data->trader_zone_id = trader_item.char_zone_id; + out_data->trader_zone_instance_id = trader_item.char_zone_instance_id; + out_data->buyer_zone_id = GetZoneID(); + out_data->buyer_zone_instance_id = GetInstanceID(); strn0cpy(out_data->trader_buy_struct.buyer_name, GetCleanName(), sizeof(out_data->trader_buy_struct.buyer_name)); + strn0cpy(out_data->trader_buy_struct.seller_name, in->seller_name, sizeof(out_data->trader_buy_struct.seller_name)); + strn0cpy(out_data->trader_buy_struct.item_name, in->item_name, sizeof(out_data->trader_buy_struct.item_name)); + strn0cpy( + out_data->trader_buy_struct.item_unique_id, + in->item_unique_id, + sizeof(out_data->trader_buy_struct.item_unique_id) + ); worldserver.SendPacket(out_server.get()); - - SendMoneyUpdate(); + LogTradingDetail("Step 3:Bazaar Purchase. Buyer checks passed, sending bazaar messaging data to trader via world.\n" + "Action: {} \n" + "Sub Action: {} \n" + "Method: {} \n" + "Item ID: {} \n" + "Item Unique ID: {} \n" + "Item Name: {} \n" + "Price: {} \n" + "Quantity: {} \n" + "Charges: {} \n" + "Augment One: {} \n" + "Augment Two: {} \n" + "Augment Three: {} \n" + "Augment Four: {} \n" + "Augment Five: {} \n" + "Augment Six: {} \n" + "Already Sold: {} \n" + "DB ID: {} \n" + "Trader ID: {} \n" + "Trader: {} \n" + "Trader Zone ID {} \n" + "Trader Zone Instance ID {} \n" + "Buyer ID: {} \n" + "Buyer: {} \n", + out_data->trader_buy_struct.action, + out_data->trader_buy_struct.sub_action, + out_data->trader_buy_struct.method, + out_data->trader_buy_struct.item_id, + out_data->trader_buy_struct.item_unique_id, + out_data->trader_buy_struct.item_name, + out_data->trader_buy_struct.price, + out_data->trader_buy_struct.quantity, + out_data->item_charges, + out_data->item_aug_1, + out_data->item_aug_2, + out_data->item_aug_3, + out_data->item_aug_4, + out_data->item_aug_5, + out_data->item_aug_6, + out_data->trader_buy_struct.already_sold, + out_data->id, + out_data->trader_buy_struct.trader_id, + out_data->trader_buy_struct.seller_name, + out_data->trader_zone_id, + out_data->trader_zone_instance_id, + out_data->buyer_id, + out_data->trader_buy_struct.buyer_name); } void Client::SetBuyerWelcomeMessage(const char *welcome_message) @@ -3859,3 +3738,60 @@ void Client::CancelTraderTradeWindow() auto end_session = new EQApplicationPacket(OP_ShopEnd); FastQueuePacket(&end_session); } + +void Client::AddDataToMerchantList(int16 slot_id, uint32 item_id, int32 quantity, const std::string &item_unique_id) +{ + auto list = GetTraderMerchantList(); + list->emplace(std::pair(slot_id, std::make_tuple(item_id, quantity, item_unique_id))); +} + +int16 Client::GetNextFreeSlotFromMerchantList() +{ + auto list = GetTraderMerchantList(); + for (auto const &[slot_id, merchant_data] : *list) { + auto [item_id, quantity, item_unique_id] = merchant_data; + if (item_id == 0) { + return slot_id; + } + } + + if (list->size() == GetInv().GetLookup()->InventoryTypeSize.Bazaar) { + return INVALID_INDEX; + } + + return list->size() + 1; +} + +std::tuple Client::GetDataFromMerchantListByMerchantSlotId(int16 slot_id) +{ + auto list = GetTraderMerchantList(); + return list->contains(slot_id) ? list->at(slot_id) : std::make_tuple(0, 0, "0000000000000000"); +} + +int16 Client::GetSlotFromMerchantListByItemUniqueId(const std::string &unique_id) +{ + auto list = GetTraderMerchantList(); + + for (auto [slot_id, merchant_data] : *list) { + auto [item_id, quantity, item_unique_id] = merchant_data; + if (item_unique_id == unique_id) { + return slot_id; + } + } + + return INVALID_INDEX; +} + +std::pair> Client::GetDataFromMerchantListByItemUniqueId(const std::string &unique_id) +{ + auto list = GetTraderMerchantList(); + + for (auto [slot_id, merchant_data] : *list) { + auto [item_id, quantity, item_unique_id] = merchant_data; + if (item_unique_id == unique_id) { + return { slot_id, merchant_data }; + } + } + + return std::make_pair(INVALID_INDEX, std::make_tuple(0, 0, "0000000000000000")); +} diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index b73a55b6b..c81266957 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -23,7 +23,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "common/misc_functions.h" #include "common/patches/patches.h" #include "common/profanity_manager.h" +#include "common/repositories/account_repository.h" #include "common/repositories/guild_tributes_repository.h" +#include "common/repositories/character_offline_transactions_repository.h" +#include "common/repositories/offline_character_sessions_repository.h" #include "common/rulesys.h" #include "common/say_link.h" #include "common/server_reload_types.h" @@ -54,6 +57,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include "common/repositories/account_repository.h" +#include "common/repositories/character_offline_transactions_repository.h" + extern EntityList entity_list; extern Zone *zone; extern volatile bool is_zone_loaded; @@ -3766,62 +3772,268 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } case ServerOP_BazaarPurchase: { - auto in = (BazaarPurchaseMessaging_Struct *) pack->pBuffer; - auto trader_pc = entity_list.GetClientByCharID(in->trader_buy_struct.trader_id); - if (!trader_pc) { - LogTrading("Request trader_id [{}] could not be found in zone_id [{}]", - in->trader_buy_struct.trader_id, - zone->GetZoneID() - ); - return; - } + auto in = reinterpret_cast(pack->pBuffer); + switch (in->transaction_status) { + case BazaarPurchaseBuyerCompleteSendToSeller: { + auto trader_pc = entity_list.GetClientByCharID(in->trader_buy_struct.trader_id); + if (!trader_pc) { + LogTrading( + "Request trader_id [{}] could not be found in zone_id [{}] instance_id [{}]", + in->trader_buy_struct.trader_id, + zone->GetZoneID(), + zone->GetInstanceID() + ); + return; + } - if (trader_pc->IsThereACustomer()) { - auto customer = entity_list.GetClientByID(trader_pc->GetCustomerID()); - if (customer) { - customer->CancelTraderTradeWindow(); + auto item = trader_pc->FindTraderItemByUniqueID(in->trader_buy_struct.item_unique_id); + if (!item) { + in->transaction_status = BazaarPurchaseTraderFailed; + TraderRepository::UpdateActiveTransaction(database, in->id, false); + worldserver.SendPacket(pack); + break; + } + + //if there is a customer currently browsing, close to ensure no conflict of purchase + if (trader_pc->IsThereACustomer()) { + auto customer = entity_list.GetClientByID(trader_pc->GetCustomerID()); + if (customer) { + customer->CancelTraderTradeWindow(); + } + } + + //Update the trader's db entries + if (item->IsStackable() && in->item_quantity != in->item_charges) { + TraderRepository::UpdateQuantity(database, in->trader_buy_struct.item_unique_id, item->GetCharges() - in->item_quantity); + LogTradingDetail( + "Step 4:Bazaar Purchase. Decreased database id {} from [{}] to [{}] charges", + in->trader_buy_struct.item_id, + item->GetCharges(), + item->GetCharges() - in->item_quantity + ); + } + else { + TraderRepository::DeleteOne(database, in->id); + LogTradingDetail( + "Step 4:Bazaar Purchase. Deleted database id [{}] because database quantity [{}] equals [{}] purchased quantity", + in->trader_buy_struct.item_id, + item->GetCharges(), + item->GetCharges() - in->item_quantity + ); + } + + //at this time, buyer checks ok, seller checks ok. + //perform actions to trader + uint64 total_cost = static_cast(in->trader_buy_struct.price) * static_cast(in->item_quantity); + if (!trader_pc->RemoveItemByItemUniqueId(in->trader_buy_struct.item_unique_id, in->item_quantity)) { + LogTradingDetail( + "Failed to remove item {} quantity [{}] from trader [{}]", + in->trader_buy_struct.item_unique_id, + in->item_quantity, + trader_pc->CharacterID() + ); + in->transaction_status = BazaarPurchaseTraderFailed; + TraderRepository::UpdateActiveTransaction(database, in->id, false); + worldserver.SendPacket(pack); + break; + } + + LogTradingDetail( + "Step 5:Bazaar Purchase. Removed from inventory of Trader [{}] for sale of [{}] {}{}", + trader_pc->CharacterID(), + in->item_quantity, + in->item_quantity > 1 ? fmt::format("{}s", in->trader_buy_struct.item_name) + : in->trader_buy_struct.item_name, + item->GetItem()->MaxCharges > 0 ? fmt::format(" with charges of [{}]", in->item_charges) + : std::string("") + ); + + trader_pc->AddMoneyToPP(total_cost, true); + + //Update the trader to indicate the sale has completed + EQApplicationPacket outapp(OP_Trader, sizeof(TraderBuy_Struct)); + auto data = reinterpret_cast(outapp.pBuffer); + + memcpy(data, &in->trader_buy_struct, sizeof(TraderBuy_Struct)); + trader_pc->QueuePacket(&outapp); + + if (item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) { + auto e = PlayerEvent::TraderSellEvent{ + .item_id = item->GetID(), + .augment_1_id = item->GetAugmentItemID(0), + .augment_2_id = item->GetAugmentItemID(1), + .augment_3_id = item->GetAugmentItemID(2), + .augment_4_id = item->GetAugmentItemID(3), + .augment_5_id = item->GetAugmentItemID(4), + .augment_6_id = item->GetAugmentItemID(5), + .item_name = in->trader_buy_struct.item_name, + .buyer_id = in->buyer_id, + .buyer_name = in->trader_buy_struct.buyer_name, + .price = in->trader_buy_struct.price, + .quantity = in->item_quantity, + .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); + + LogTradingDetail("Step 6:Bazaar Purchase. Purchase checks complete for trader. Send Success to buyer via world."); + + break; + } + case BazaarPurchaseTraderFailed: { + auto buyer = entity_list.GetClientByCharID(in->buyer_id); + if (!buyer) { + LogTrading( + "Requested buyer_id [{}] could not be found in zone_id [{}] instance_id [{}]", + in->trader_buy_struct.trader_id, + zone->GetZoneID(), + zone->GetInstanceID() + ); + return; + } + + // return buyer's money including the fee + uint64 total_cost = + static_cast(in->trader_buy_struct.price) * static_cast(in->item_quantity); + uint64 fee = std::round(total_cost * RuleR(Bazaar, ParcelDeliveryCostMod)); + buyer->AddMoneyToPP(total_cost + fee, false); + buyer->SendMoneyUpdate(); + + buyer->Message(Chat::Red, "Bazaar purchased failed. Returning your money."); + LogTradingDetail( + "Bazaar Purchase Failed. Returning money [{}] + fee [{}] to Buyer [{}]", + total_cost, + fee, + buyer->CharacterID() + ); + buyer->TradeRequestFailed(in->trader_buy_struct); + break; + } + case BazaarPurchaseSuccess: { + auto buyer = entity_list.GetClientByCharID(in->buyer_id); + if (!buyer) { + LogTrading( + "Requested buyer_id [{}] could not be found in zone_id [{}] instance_id [{}]", + in->trader_buy_struct.trader_id, + zone->GetZoneID(), + zone->GetInstanceID() + ); + return; + } + uint64 total_cost = + static_cast(in->trader_buy_struct.price) * static_cast(in->item_quantity); + + if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { + auto e = PlayerEvent::TraderPurchaseEvent{ + .item_id = in->trader_buy_struct.item_id, + .augment_1_id = in->item_aug_1, + .augment_2_id = in->item_aug_2, + .augment_3_id = in->item_aug_3, + .augment_4_id = in->item_aug_4, + .augment_5_id = in->item_aug_5, + .augment_6_id = in->item_aug_6, + .item_name = in->trader_buy_struct.item_name, + .trader_id = in->trader_buy_struct.trader_id, + .trader_name = in->trader_buy_struct.seller_name, + .price = in->trader_buy_struct.price, + .quantity = in->item_quantity, + .charges = in->item_charges, + .total_cost = total_cost, + .player_money_balance = buyer->GetCarriedMoney(), + }; + + RecordPlayerEventLogWithClient(buyer, PlayerEvent::TRADER_PURCHASE, e); + } + + auto item = database.GetItem(in->trader_buy_struct.item_id); + auto quantity = in->item_quantity; + if (item->MaxCharges > 0) { + quantity = in->item_charges; + } + + //Send the item via parcel + CharacterParcelsRepository::CharacterParcels parcel_out{}; + parcel_out.from_name = in->trader_buy_struct.seller_name; + parcel_out.note = "Delivered from a Bazaar Purchase"; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity; + parcel_out.item_id = in->trader_buy_struct.item_id; + parcel_out.item_unique_id = in->trader_buy_struct.item_unique_id; + parcel_out.aug_slot_1 = in->item_aug_1; + parcel_out.aug_slot_2 = in->item_aug_2; + parcel_out.aug_slot_3 = in->item_aug_3; + parcel_out.aug_slot_4 = in->item_aug_4; + parcel_out.aug_slot_5 = in->item_aug_5; + parcel_out.aug_slot_6 = in->item_aug_6; + parcel_out.char_id = buyer->CharacterID(); + parcel_out.slot_id = buyer->FindNextFreeParcelSlot(buyer->CharacterID()); + parcel_out.id = 0; + + CharacterParcelsRepository::InsertOne(database, parcel_out); + + if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + e.from_player_name = parcel_out.from_name; + e.to_player_name = buyer->GetCleanName(); + e.item_id = parcel_out.item_id; + e.augment_1_id = parcel_out.aug_slot_1; + e.augment_2_id = parcel_out.aug_slot_2; + e.augment_3_id = parcel_out.aug_slot_3; + e.augment_4_id = parcel_out.aug_slot_4; + e.augment_5_id = parcel_out.aug_slot_5; + e.augment_6_id = parcel_out.aug_slot_6; + e.quantity = in->item_quantity; + e.charges = in->item_charges; + e.sent_date = parcel_out.sent_date; + + RecordPlayerEventLogWithClient(buyer, PlayerEvent::PARCEL_SEND, e); + } + + Parcel_Struct ps{}; + ps.item_slot = parcel_out.slot_id; + strn0cpy(ps.send_to, buyer->GetCleanName(), sizeof(ps.send_to)); + buyer->SendParcelDeliveryToWorld(ps); + + LogTradingDetail("Step 7:Bazaar Purchase. Sent parcel to Buyer [{}] for purchase of [{}] {}{}", + buyer->CharacterID(), + quantity, + quantity > 1 ? fmt::format("{}s", in->trader_buy_struct.item_name) : in->trader_buy_struct.item_name, + item->MaxCharges > 0 ? fmt::format(" with charges of [{}]", in->item_charges) : std::string("") + ); + + //Update the buyer to indicate the sale has completed + EQApplicationPacket outapp(OP_Trader, sizeof(TraderBuy_Struct)); + auto data = reinterpret_cast(outapp.pBuffer); + + memcpy(data, &in->trader_buy_struct, sizeof(TraderBuy_Struct)); + buyer->ReturnTraderReq(&outapp, in->item_quantity, in->trader_buy_struct.item_id); + LogTradingDetail("Step 8:Bazaar Purchase. Purchase complete. Sending update packet to buyer."); + + break; + } + default: { } } - auto item_sn = Strings::ToUnsignedBigInt(in->trader_buy_struct.serial_number); - auto outapp = std::make_unique(OP_Trader, static_cast(sizeof(TraderBuy_Struct))); - auto data = (TraderBuy_Struct *) outapp->pBuffer; - - memcpy(data, &in->trader_buy_struct, sizeof(TraderBuy_Struct)); - - if (trader_pc->ClientVersion() < EQ::versions::ClientVersion::RoF) { - data->price = in->trader_buy_struct.price * in->trader_buy_struct.quantity; - } - - TraderRepository::UpdateActiveTransaction(database, in->id, false); - - auto item = trader_pc->FindTraderItemBySerialNumber(item_sn); - - if (item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) { - auto e = PlayerEvent::TraderSellEvent{ - .item_id = item ? item->GetID() : 0, - .augment_1_id = item->GetAugmentItemID(0), - .augment_2_id = item->GetAugmentItemID(1), - .augment_3_id = item->GetAugmentItemID(2), - .augment_4_id = item->GetAugmentItemID(3), - .augment_5_id = item->GetAugmentItemID(4), - .augment_6_id = item->GetAugmentItemID(5), - .item_name = in->trader_buy_struct.item_name, - .buyer_id = in->buyer_id, - .buyer_name = in->trader_buy_struct.buyer_name, - .price = in->trader_buy_struct.price, - .quantity = in->trader_buy_struct.quantity, - .charges = item ? item->IsStackable() ? 1 : item->GetCharges() : 0, - .total_cost = (in->trader_buy_struct.price * in->trader_buy_struct.quantity), - .player_money_balance = trader_pc->GetCarriedMoney(), - }; - RecordPlayerEventLogWithClient(trader_pc, PlayerEvent::TRADER_SELL, e); - } - - trader_pc->RemoveItemBySerialNumber(item_sn, in->trader_buy_struct.quantity); - trader_pc->AddMoneyToPP(in->trader_buy_struct.price * in->trader_buy_struct.quantity, true); - trader_pc->QueuePacket(outapp.get()); - break; } case ServerOP_BuyerMessaging: { @@ -4073,6 +4285,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); @@ -4133,8 +4357,126 @@ 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(); + auto session = OfflineCharacterSessionsRepository::GetByAccountId(database, r.id); + auto trader = TraderRepository::GetAccountZoneIdAndInstanceIdByAccountId(database, r.id); + const uint32 character_id = session.id ? session.character_id : trader.character_id; + + database.TransactionBegin(); + r.offline = 0; + AccountRepository::UpdateOne(database, r); + OfflineCharacterSessionsRepository::DeleteByAccountId(database, r.id); + if (character_id) { + TraderRepository::DeleteWhere(database, fmt::format("`character_id` = '{}'", character_id)); + BuyerRepository::DeleteBuyer(database, character_id); + } + + auto commit_result = database.TransactionCommit(); + if (!commit_result.Success()) { + database.TransactionRollback(); + LogError( + "Failed clearing orphaned offline session state for account [{}]: ({}) {}", + r.id, + commit_result.ErrorNumber(), + commit_result.ErrorMessage() + ); + } + + 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); + OfflineCharacterSessionsRepository::DeleteByAccountId(database, client->AccountID()); + + if (client->IsThereACustomer()) { + auto customer = entity_list.GetClientByID(client->GetCustomerID()); + if (customer) { + auto end_session = new EQApplicationPacket(OP_ShopEnd); + customer->FastQueuePacket(&end_session); + } + } + + if (client->IsTrader()) { + 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; diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 7683961f7..36e607d53 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -56,6 +56,9 @@ #include #include +#include "common/repositories/inventory_repository.h" +#include "common/repositories/inventory_snapshots_repository.h" + extern Zone* zone; ZoneDatabase database; @@ -304,19 +307,19 @@ void ZoneDatabase::DeleteWorldContainer(uint32 parent_id, uint32 zone_id) ); } -std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 char_id, int serial_number) +std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 character_id, const std::string &unique_item_id) { auto results = TraderRepository::GetWhere( database, fmt::format( - "`char_id` = '{}' AND `item_sn` = '{}' ORDER BY slot_id", - char_id, - serial_number + "`character_id` = {} AND `item_unique_id` = '{}' ORDER BY slot_id", + character_id, + unique_item_id ) ); if (results.empty()) { - LogTrading("Could not find item serial number {} for character id {}", serial_number, char_id); + LogTrading("Could not find item serial number {} for character id {}", unique_item_id, character_id); return nullptr; } @@ -338,12 +341,12 @@ std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 char database.CreateItem( item_id, charges, - results.at(0).aug_slot_1, - results.at(0).aug_slot_2, - results.at(0).aug_slot_3, - results.at(0).aug_slot_4, - results.at(0).aug_slot_5, - results.at(0).aug_slot_6 + results.at(0).augment_one, + results.at(0).augment_two, + results.at(0).augment_three, + results.at(0).augment_four, + results.at(0).augment_five, + results.at(0).augment_six ) ); if (!inst) { @@ -352,8 +355,7 @@ std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 char } inst->SetCharges(charges); - inst->SetSerialNumber(serial_number); - inst->SetMerchantSlot(serial_number); + inst->SetUniqueID(unique_item_id); inst->SetPrice(cost); if (inst->IsStackable()) { @@ -363,9 +365,9 @@ std::unique_ptr ZoneDatabase::LoadSingleTraderItem(uint32 char return std::move(inst); } -void ZoneDatabase::UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 charges, uint32 new_price) { +void ZoneDatabase::UpdateTraderItemPrice(int character_id, uint32 item_id, uint32 charges, uint32 new_price) { - LogTrading("ZoneDatabase::UpdateTraderPrice([{}], [{}], [{}], [{}])", char_id, item_id, charges, new_price); + LogTrading("ZoneDatabase::UpdateTraderPrice([{}], [{}], [{}], [{}])", character_id, item_id, charges, new_price); const EQ::ItemData *item = database.GetItem(item_id); if(!item) { @@ -373,20 +375,20 @@ void ZoneDatabase::UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 cha } if (new_price == 0) { - LogTrading("Removing Trader items from the DB for char_id [{}], item_id [{}]", char_id, item_id); + LogTrading("Removing Trader items from the DB for char_id [{}], item_id [{}]", character_id, item_id); auto results = TraderRepository::DeleteWhere( database, fmt::format( - "`char_id` = '{}' AND `item_id` = {}", - char_id, + "`character_id` = {} AND `item_id` = {}", + character_id, item_id ) ); if (!results) { LogDebug("[CLIENT] Failed to remove trader item(s): [{}] for char_id: [{}]", item_id, - char_id + character_id ); } @@ -394,23 +396,23 @@ void ZoneDatabase::UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 cha } if (!item->Stackable) { - auto results = TraderRepository::UpdateItem(database, char_id, new_price, item_id, charges); + auto results = TraderRepository::UpdateItem(database, character_id, new_price, item_id, charges); if (!results) { LogTrading( "Failed to update price for trader item [{}] for char_id: [{}]", item_id, - char_id + character_id ); } return; } - auto results = TraderRepository::UpdateItem(database, char_id, new_price, item_id, 0); + auto results = TraderRepository::UpdateItem(database, character_id, new_price, item_id, 0); if (!results) { LogTrading( "Failed to update price for trader item [{}] for char_id: [{}]", item_id, - char_id + character_id ); } } @@ -1311,269 +1313,48 @@ bool ZoneDatabase::NoRentExpired(const std::string& name) return seconds > 1800; } -bool ZoneDatabase::SaveCharacterInvSnapshot(uint32 character_id) { - uint32 time_index = time(nullptr); - std::string query = StringFormat( - "INSERT " - "INTO" - " `inventory_snapshots` " - "(`time_index`," - " `charid`," - " `slotid`," - " `itemid`," - " `charges`," - " `color`," - " `augslot1`," - " `augslot2`," - " `augslot3`," - " `augslot4`," - " `augslot5`," - " `augslot6`," - " `instnodrop`," - " `custom_data`," - " `ornamenticon`," - " `ornamentidfile`," - " `ornament_hero_model`," - " `guid`" - ") " - "SELECT" - " %u," - " `character_id`," - " `slot_id`," - " `item_id`," - " `charges`," - " `color`," - " `augment_one`," - " `augment_two`," - " `augment_three`," - " `augment_four`," - " `augment_five`," - " `augment_six`," - " `instnodrop`," - " `custom_data`," - " `ornament_icon`," - " `ornament_idfile`," - " `ornament_hero_model`," - " `guid` " - "FROM" - " `inventory` " - "WHERE" - " `character_id` = %u", - time_index, - character_id - ); - auto results = database.QueryDatabase(query); - LogInventory("[{}] ([{}])", character_id, (results.Success() ? "pass" : "fail")); - return results.Success(); -} - -int ZoneDatabase::CountCharacterInvSnapshots(uint32 character_id) { - std::string query = StringFormat( - "SELECT" - " COUNT(*) " - "FROM " - "(" - "SELECT * FROM" - " `inventory_snapshots` a " - "WHERE" - " `charid` = %u " - "GROUP BY" - " `time_index`" - ") b", - character_id - ); - auto results = QueryDatabase(query); - - if (!results.Success()) - return -1; - - auto& row = results.begin(); - - int64 count = Strings::ToBigInt(row[0]); - if (count > 2147483647) - return -2; - if (count < 0) - return -3; - - return count; -} - -void ZoneDatabase::ClearCharacterInvSnapshots(uint32 character_id, bool from_now) { - uint32 del_time = time(nullptr); - if (!from_now) { del_time -= RuleI(Character, InvSnapshotHistoryD) * 86400; } - - std::string query = StringFormat( - "DELETE " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `charid` = %u " - "AND" - " `time_index` <= %lu", - character_id, - (unsigned long)del_time - ); - QueryDatabase(query); -} - -void ZoneDatabase::ListCharacterInvSnapshots(uint32 character_id, std::list> &is_list) { - std::string query = StringFormat( - "SELECT" - " `time_index`," - " COUNT(*) " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `charid` = %u " - "GROUP BY" - " `time_index` " - "ORDER BY" - " `time_index` " - "DESC", - character_id - ); - auto results = QueryDatabase(query); - - if (!results.Success()) - return; - - for (auto row : results) - is_list.emplace_back(std::pair(Strings::ToUnsignedInt(row[0]), Strings::ToInt(row[1]))); -} - -bool ZoneDatabase::ValidateCharacterInvSnapshotTimestamp(uint32 character_id, uint32 timestamp) { - if (!character_id || !timestamp) - return false; - - std::string query = StringFormat( - "SELECT" - " * " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `charid` = %u " - "AND" - " `time_index` = %u " - "LIMIT 1", - character_id, - timestamp - ); - auto results = QueryDatabase(query); - - if (!results.Success() || results.RowCount() == 0) +bool ZoneDatabase::SaveCharacterInvSnapshot(uint32 character_id) +{ + if (!InventorySnapshotsRepository::SaveCharacterInvSnapshot(database, character_id)) { return false; + } return true; } -void ZoneDatabase::ParseCharacterInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &parse_list) { - std::string query = StringFormat( - "SELECT" - " `slotid`," - " `itemid` " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `charid` = %u " - "AND" - " `time_index` = %u " - "ORDER BY" - " `slotid`", - character_id, - timestamp - ); - auto results = QueryDatabase(query); - - if (!results.Success()) - return; - - for (auto row : results) - parse_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); +int ZoneDatabase::CountCharacterInvSnapshots(uint32 character_id) +{ + return InventorySnapshotsRepository::CountCharacterInvSnapshots(*this, character_id); } -void ZoneDatabase::DivergeCharacterInvSnapshotFromInventory(uint32 character_id, uint32 timestamp, std::list> &compare_list) { - std::string query = StringFormat( - "SELECT" - " slotid," - " itemid " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `time_index` = %u " - "AND" - " `charid` = %u " - "AND" - " `slotid` NOT IN " - "(" - "SELECT" - " a.`slotid` " - "FROM" - " `inventory_snapshots` a " - "JOIN" - " `inventory` b " - "USING" - " (`slot_id`, `item_id`) " - "WHERE" - " a.`time_index` = %u " - "AND" - " a.`charid` = %u " - "AND" - " b.`character_id` = %u" - ")", - timestamp, - character_id, - timestamp, - character_id, - character_id - ); - auto results = QueryDatabase(query); - - if (!results.Success()) - return; - - for (auto row : results) - compare_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); +void ZoneDatabase::ClearCharacterInvSnapshots(uint32 character_id, bool from_now) +{ + InventorySnapshotsRepository::ClearCharacterInvSnapshots(*this, character_id, from_now); } -void ZoneDatabase::DivergeCharacterInventoryFromInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &compare_list) { - std::string query = StringFormat( - "SELECT" - " `slotid`," - " `itemid` " - "FROM" - " `inventory` " - "WHERE" - " `character_id` = %u " - "AND" - " `slotid` NOT IN " - "(" - "SELECT" - " a.`slotid` " - "FROM" - " `inventory` a " - "JOIN" - " `inventory_snapshots` b " - "USING" - " (`slotid`, `itemid`) " - "WHERE" - " b.`time_index` = %u " - "AND" - " b.`charid` = %u " - "AND" - " a.`character_id` = %u" - ")", - character_id, - timestamp, - character_id, - character_id - ); - auto results = QueryDatabase(query); +void ZoneDatabase::ListCharacterInvSnapshots(uint32 character_id, std::list> &is_list) +{ + InventorySnapshotsRepository::ListCharacterInvSnapshots(*this, character_id, is_list); +} - if (!results.Success()) - return; +bool ZoneDatabase::ValidateCharacterInvSnapshotTimestamp(uint32 character_id, uint32 timestamp) +{ + return InventorySnapshotsRepository::ValidateCharacterInvSnapshotTimestamp(*this, character_id, timestamp); +} - for (auto row : results) - compare_list.emplace_back(std::pair(Strings::ToInt(row[0]), Strings::ToUnsignedInt(row[1]))); +void ZoneDatabase::ParseCharacterInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &parse_list) +{ + InventorySnapshotsRepository::ParseCharacterInvSnapshot(*this, character_id, timestamp, parse_list); +} + +void ZoneDatabase::DivergeCharacterInvSnapshotFromInventory(uint32 character_id, uint32 timestamp, std::list> &compare_list) +{ + InventorySnapshotsRepository::DivergeCharacterInvSnapshotFromInventory(*this, character_id, timestamp, compare_list); +} + +void ZoneDatabase::DivergeCharacterInventoryFromInvSnapshot(uint32 character_id, uint32 timestamp, std::list> &compare_list) +{ + InventorySnapshotsRepository::DivergeCharacterInventoryFromInvSnapshot(*this, character_id, timestamp, compare_list); } bool ZoneDatabase::RestoreCharacterInvSnapshot(uint32 character_id, uint32 timestamp) { @@ -1584,73 +1365,7 @@ bool ZoneDatabase::RestoreCharacterInvSnapshot(uint32 character_id, uint32 times return false; } - std::string query = StringFormat( - "DELETE " - "FROM" - " `inventory` " - "WHERE" - " `character_id` = %u", - character_id - ); - auto results = database.QueryDatabase(query); - if (!results.Success()) - return false; - - query = StringFormat( - "INSERT " - "INTO" - " `inventory` " - "(`character_id`," - " `slot_id`," - " `item_id`," - " `charges`," - " `color`," - " `augment_one`," - " `augment_two`," - " `augment_three`," - " `augment_four`," - " `augment_five`," - " `augment_six`," - " `instnodrop`," - " `custom_data`," - " `ornament_icon`," - " `ornament_idfile`," - " `ornament_hero_model`," - " `guid` " - ") " - "SELECT" - " `charid`," - " `slotid`," - " `itemid`," - " `charges`," - " `color`," - " `augslot1`," - " `augslot2`," - " `augslot3`," - " `augslot4`," - " `augslot5`," - " `augslot6`," - " `instnodrop`," - " `custom_data`," - " `ornamenticon`," - " `ornamentidfile`," - " `ornament_hero_model`, " - " `guid` " - "FROM" - " `inventory_snapshots` " - "WHERE" - " `charid` = %u " - "AND" - " `time_index` = %u", - character_id, - timestamp - ); - results = database.QueryDatabase(query); - - LogInventory("[{}] snapshot for [{}] @ [{}]", - (results.Success() ? "restored" : "failed to restore"), character_id, timestamp); - - return results.Success(); + return InventorySnapshotsRepository::RestoreCharacterInvSnapshot(database, character_id, timestamp); } const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load /*= false*/) diff --git a/zone/zonedb.h b/zone/zonedb.h index 5dee242fb..a94c2fc37 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -389,11 +389,11 @@ public: /* Traders */ void SaveTraderItem(uint32 char_id,uint32 itemid,uint32 uniqueid, int32 charges,uint32 itemcost,uint8 slot); void UpdateTraderItemCharges(int char_id, uint32 ItemInstID, int32 charges); - void UpdateTraderItemPrice(int char_id, uint32 item_id, uint32 charges, uint32 new_price); + void UpdateTraderItemPrice(int character_id, uint32 item_id, uint32 charges, uint32 new_price); void DeleteTraderItem(uint32 char_id); void DeleteTraderItem(uint32 char_id,uint16 slot_id); - std::unique_ptr LoadSingleTraderItem(uint32 char_id, int serial_number); + std::unique_ptr LoadSingleTraderItem(uint32 char_id, const std::string &serial_number); Trader_Struct* LoadTraderItem(uint32 char_id); TraderCharges_Struct* LoadTraderItemWithCharges(uint32 char_id);