From fcffc6b3d4446f6dbf0dba582ea47506faa0c859 Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Sat, 20 Apr 2024 23:15:56 -0300 Subject: [PATCH] [Feature] Add Parcel Feature for RoF2 Clients (#4198) * Add Parcel Feature Add the parcel system for RoF2 client * Fixed a duplicate define * Reformat reformating and review changes * Further Formatting * Memory Mgmt Updates Refactored to using unique_ptr/make_unique/etc to avoid manual memory mgmt. Other format changes * Refactor db structure Refactor for db structure of parcels to character_parcels Removal of parcel_merchants Addition of npc_types.is_parcel_merchant Cleanup as a result * Refactor to use item id 99990 for money transfers. Removed the money string function as a result, though simplified the messaging related to money. Other updates based on feedback. * Move prune routine out of scheduler and into a world process. Removed RuleI from #define * Update * Update database.cpp * Update database_update_manifest.cpp * Update main.cpp * Update client_process.cpp * Update parcels.cpp * Remove parcel merchant content to optional sql instead of manifest. --------- Co-authored-by: Akkadius --- client_files/export/main.cpp | 8 +- client_files/import/main.cpp | 8 +- common/CMakeLists.txt | 2 + common/database.cpp | 41 + common/database.h | 1 + common/database/database_update_manifest.cpp | 26 + common/database_schema.h | 2 + common/emu_oplist.h | 5 + common/eq_constants.h | 6 + common/eq_packet_structs.h | 115 ++- common/events/player_event_logs.cpp | 7 +- common/events/player_events.h | 72 +- common/patches/rof.cpp | 28 - common/patches/rof2.cpp | 173 ++-- common/patches/rof2_ops.h | 3 +- common/patches/rof2_structs.h | 68 +- common/patches/rof_ops.h | 2 - common/patches/rof_structs.h | 18 +- common/patches/sod.cpp | 28 + common/patches/sod_ops.h | 2 + common/patches/sod_structs.h | 14 +- common/patches/sof.cpp | 28 + common/patches/sof_ops.h | 2 + common/patches/sof_structs.h | 14 +- common/patches/titanium.cpp | 28 + common/patches/titanium_ops.h | 2 + common/patches/titanium_structs.h | 6 +- common/patches/uf.cpp | 28 + common/patches/uf_ops.h | 2 + common/patches/uf_structs.h | 6 +- .../base/base_character_parcels_repository.h | 463 +++++++++++ .../base/base_npc_types_repository.h | 12 + .../character_parcels_repository.h | 83 ++ common/ruletypes.h | 10 + common/server_event_scheduler.h | 2 +- common/servertalk.h | 3 + common/version.h | 2 +- shared_memory/main.cpp | 8 +- utils/patches/patch_RoF2.conf | 4 + .../2024_04_20_Parcel_Merchant_Updates.sql | 3 + world/main.cpp | 18 + world/world_event_scheduler.cpp | 10 +- world/zoneserver.cpp | 13 + zone/CMakeLists.txt | 1 + zone/client.cpp | 16 +- zone/client.h | 40 +- zone/client_packet.cpp | 158 +++- zone/client_packet.h | 3 + zone/client_process.cpp | 17 + zone/command.cpp | 2 + zone/command.h | 2 + zone/gm_commands/parcels.cpp | 307 +++++++ zone/npc.h | 1 + zone/parcels.cpp | 752 ++++++++++++++++++ zone/string_ids.h | 13 + zone/worldserver.cpp | 45 ++ zone/zonedb.cpp | 1 + zone/zonedump.h | 1 + 58 files changed, 2505 insertions(+), 230 deletions(-) create mode 100644 common/repositories/base/base_character_parcels_repository.h create mode 100644 common/repositories/character_parcels_repository.h create mode 100644 utils/sql/git/optional/2024_04_20_Parcel_Merchant_Updates.sql create mode 100644 zone/gm_commands/parcels.cpp create mode 100644 zone/parcels.cpp diff --git a/client_files/export/main.cpp b/client_files/export/main.cpp index 9e85ad24d..79807e1ef 100644 --- a/client_files/export/main.cpp +++ b/client_files/export/main.cpp @@ -31,11 +31,13 @@ #include "../../common/path_manager.h" #include "../../common/repositories/skill_caps_repository.h" #include "../../common/file.h" +#include "../../common/events/player_event_logs.h" -EQEmuLogSys LogSys; +EQEmuLogSys LogSys; WorldContentService content_service; -ZoneStore zone_store; -PathManager path; +ZoneStore zone_store; +PathManager path; +PlayerEventLogs player_event_logs; void ExportSpells(SharedDatabase *db); void ExportSkillCaps(SharedDatabase *db); diff --git a/client_files/import/main.cpp b/client_files/import/main.cpp index a48ab45ee..0d8010221 100644 --- a/client_files/import/main.cpp +++ b/client_files/import/main.cpp @@ -29,11 +29,13 @@ #include "../../common/path_manager.h" #include "../../common/repositories/base_data_repository.h" #include "../../common/file.h" +#include "../../common/events/player_event_logs.h" -EQEmuLogSys LogSys; +EQEmuLogSys LogSys; WorldContentService content_service; -ZoneStore zone_store; -PathManager path; +ZoneStore zone_store; +PathManager path; +PlayerEventLogs player_event_logs; void ImportSpells(SharedDatabase *db); void ImportSkillCaps(SharedDatabase *db); diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index e297d6031..6de4825ab 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -177,6 +177,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_parcels_repository.h repositories/base/base_character_peqzone_flags_repository.h repositories/base/base_character_pet_buffs_repository.h repositories/base/base_character_pet_info_repository.h @@ -357,6 +358,7 @@ SET(repositories repositories/character_leadership_abilities_repository.h repositories/character_material_repository.h repositories/character_memmed_spells_repository.h + repositories/character_parcels_repository.h repositories/character_peqzone_flags_repository.h repositories/character_pet_buffs_repository.h repositories/character_pet_info_repository.h diff --git a/common/database.cpp b/common/database.cpp index f0ceea192..141ce1fee 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -35,6 +35,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_repository.h" #include "../common/repositories/character_skills_repository.h" #include "../common/repositories/data_buckets_repository.h" #include "../common/repositories/group_id_repository.h" @@ -49,6 +50,7 @@ #include "../common/repositories/raid_members_repository.h" #include "../common/repositories/reports_repository.h" #include "../common/repositories/variables_repository.h" +#include "../common/events/player_event_logs.h" // Disgrace: for windows compile #ifdef _WINDOWS @@ -2042,3 +2044,42 @@ void Database::Decode(std::string &in) in.at(i) -= char('0'); } }; + +void Database::PurgeCharacterParcels() +{ + auto filter = fmt::format("sent_date < (NOW() - INTERVAL {} DAY)", RuleI(Parcel, ParcelPruneDelay)); + auto results = CharacterParcelsRepository::GetWhere(*this, filter); + auto prune = CharacterParcelsRepository::DeleteWhere(*this, filter); + + PlayerEvent::ParcelDelete pd{}; + PlayerEventLogsRepository::PlayerEventLogs pel{}; + pel.event_type_id = PlayerEvent::PARCEL_DELETE; + pel.event_type_name = PlayerEvent::EventName[pel.event_type_id]; + std::stringstream ss; + for (auto const &r: results) { + pd.from_name = r.from_name; + pd.item_id = r.item_id; + pd.note = r.note; + pd.quantity = r.quantity; + pd.sent_date = r.sent_date; + pd.char_id = r.char_id; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + pd.serialize(ar); + } + + pel.event_data = ss.str(); + pel.created_at = std::time(nullptr); + + player_event_logs.AddToQueue(pel); + + ss.str(""); + ss.clear(); + } + + LogInfo( + "Purged [{}] parcels that were over [{}] days old.", + results.size(), + RuleI(Parcel, ParcelPruneDelay) + ); +} diff --git a/common/database.h b/common/database.h index bc5d38c13..a5a388f19 100644 --- a/common/database.h +++ b/common/database.h @@ -267,6 +267,7 @@ public: void SourceDatabaseTableFromUrl(const std::string& table_name, const std::string& url); void SourceSqlFromUrl(const std::string& url); + void PurgeCharacterParcels(); void Encode(std::string &in); void Decode(std::string &in); diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 9e77b67d4..52d453b1d 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5494,6 +5494,32 @@ ALTER TABLE `lootdrop_entries` ADD `content_flags` varchar(100) NULL; ALTER TABLE `lootdrop_entries` ADD `content_flags_disabled` varchar(100) NULL; )", .content_schema_update = true + }, + ManifestEntry{ + .version = 9271, + .description = "2024_03_10_parcel_implementation.sql", + .check = "SHOW TABLES LIKE 'character_parcels'", + .condition = "empty", + .match = "", + .sql = R"(CREATE TABLE `character_parcels` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `char_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `item_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `slot_id` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `quantity` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `from_name` VARCHAR(64) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci', + `note` VARCHAR(1024) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci', + `sent_date` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `data_constraint` (`slot_id`, `char_id`) USING BTREE + ) + COLLATE='latin1_swedish_ci' + ENGINE=InnoDB + AUTO_INCREMENT=1; + + ALTER TABLE `npc_types` + ADD COLUMN `is_parcel_merchant` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `keeps_sold_items`; + )" } // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ diff --git a/common/database_schema.h b/common/database_schema.h index cb9cd13c7..e7c554179 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -59,6 +59,7 @@ namespace DatabaseSchema { {"character_leadership_abilities", "id"}, {"character_material", "id"}, {"character_memmed_spells", "id"}, + {"character_parcels", "char_id"}, {"character_pet_buffs", "char_id"}, {"character_pet_info", "char_id"}, {"character_pet_inventory", "char_id"}, @@ -128,6 +129,7 @@ namespace DatabaseSchema { "character_leadership_abilities", "character_material", "character_memmed_spells", + "character_parcels", "character_pet_buffs", "character_pet_info", "character_pet_inventory", diff --git a/common/emu_oplist.h b/common/emu_oplist.h index b7c30dc8c..c45cac1ae 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -510,6 +510,11 @@ N(OP_ShopEndConfirm), N(OP_ShopItem), N(OP_ShopPlayerBuy), N(OP_ShopPlayerSell), +N(OP_ShopSendParcel), +N(OP_ShopDeleteParcel), +N(OP_ShopRespondParcel), +N(OP_ShopRetrieveParcel), +N(OP_ShopParcelIcon), N(OP_ShopRequest), N(OP_SimpleMessage), N(OP_SkillUpdate), diff --git a/common/eq_constants.h b/common/eq_constants.h index 31c02aaa3..cbbe3a3fc 100644 --- a/common/eq_constants.h +++ b/common/eq_constants.h @@ -1131,4 +1131,10 @@ namespace LeadershipAbilitySlot { constexpr uint16 HealthOfTargetsTarget = 14; } +#define PARCEL_SEND_ITEMS 0 +#define PARCEL_SEND_MONEY 1 +#define PARCEL_MONEY_ITEM_ID 99990 // item id of money +#define PARCEL_LIMIT 5 +#define PARCEL_BEGIN_SLOT 1 + #endif /*COMMON_EQ_CONSTANTS_H*/ diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 3f3b508b8..51b6efea7 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -27,6 +27,8 @@ #include "../common/version.h" #include "emu_constants.h" #include "textures.h" +#include "../cereal/include/cereal/archives/binary.hpp" +#include "../cereal/include/cereal/types/string.hpp" static const uint32 BUFF_COUNT = 42; @@ -1533,20 +1535,32 @@ struct ExpUpdate_Struct ** Packet Types: See ItemPacketType enum ** */ -enum ItemPacketType -{ - ItemPacketViewLink = 0x00, - ItemPacketMerchant = 0x64, - ItemPacketTradeView = 0x65, - ItemPacketLoot = 0x66, - ItemPacketTrade = 0x67, - ItemPacketCharInventory = 0x69, - ItemPacketLimbo = 0x6A, - ItemPacketWorldContainer = 0x6B, - ItemPacketTributeItem = 0x6C, - ItemPacketGuildTribute = 0x6D, - ItemPacketCharmUpdate = 0x6E, // noted as incorrect - ItemPacketInvalid = 0xFF +enum ItemPacketType { + ItemPacketViewLink = 0x00, + ItemPacketMerchant = 0x64, + ItemPacketTradeView = 0x65, + ItemPacketLoot = 0x66, + ItemPacketTrade = 0x67, + ItemPacketCharInventory = 0x69, + ItemPacketLimbo = 0x6A, + ItemPacketWorldContainer = 0x6B, + ItemPacketTributeItem = 0x6C, + ItemPacketGuildTribute = 0x6D, + ItemPacketCharmUpdate = 0x6E, // noted as incorrect + ItemPacketRecovery = 0x71, + ItemPacketParcel = 0x73, + ItemPacketInvalid = 0xFF +}; + +enum MerchantWindowTabDisplay { + None = 0x00, + SellBuy = 0x01, + Recover = 0x02, + SellBuyRecover = 0x03, + Parcel = 0x04, + SellBuyParcel = 0x05, + RecoverParcel = 0x06, + SellBuyRecoverParcel = 0x07 }; //enum ItemPacketType @@ -2057,12 +2071,75 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; -/*008*/ uint32 command; //1=open, 0=cancel/close -/*012*/ float rate; //cost multiplier, dosent work anymore +struct MerchantClick_Struct +{ + /*000*/ uint32 npc_id; // Merchant NPC's entity id + /*004*/ uint32 player_id; + /*008*/ uint32 command; // 1=open, 0=cancel/close + /*012*/ float rate; // cost multiplier, dosent work anymore + /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels + /*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client + /*024*/ }; + +enum MerchantActions { + Close = 0, + Open = 1 +}; + +struct Parcel_Struct +{ + /*000*/ uint32 npc_id; + /*004*/ uint32 item_slot; + /*008*/ uint32 quantity; + /*012*/ uint32 money_flag; + /*016*/ char send_to[64]; + /*080*/ char note[128]; + /*208*/ uint32 unknown_208; + /*212*/ uint32 unknown_212; + /*216*/ uint32 unknown_216; +}; + +struct ParcelRetrieve_Struct +{ + uint32 merchant_entity_id; + uint32 player_entity_id; + uint32 parcel_slot_id; + uint32 parcel_item_id; +}; + +struct ParcelMessaging_Struct { + ItemPacketType packet_type; + std::string serialized_item; + uint32 sent_time; + std::string player_name; + std::string note; + uint32 slot_id; + + template + void serialize(Archive &archive) + { + archive( + CEREAL_NVP(packet_type), + CEREAL_NVP(serialized_item), + CEREAL_NVP(sent_time), + CEREAL_NVP(player_name), + CEREAL_NVP(note), + CEREAL_NVP(slot_id) + ); + } +}; + +struct ParcelIcon_Struct { + uint32 status; //0 off 1 on 2 overlimit +}; + +enum ParcelIconActions { + IconOff = 0, + IconOn = 1, + Overlimit = 2 +}; + /* Unknowns: 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE diff --git a/common/events/player_event_logs.cpp b/common/events/player_event_logs.cpp index 557b5830a..f472264fb 100644 --- a/common/events/player_event_logs.cpp +++ b/common/events/player_event_logs.cpp @@ -697,8 +697,11 @@ void PlayerEventLogs::SetSettingsDefaults() m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1; m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1; m_settings[PlayerEvent::ITEM_CREATION].event_enabled = 1; - m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1; - m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1; + m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1; + m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1; + m_settings[PlayerEvent::PARCEL_SEND].event_enabled = 1; + m_settings[PlayerEvent::PARCEL_RETRIEVE].event_enabled = 1; + m_settings[PlayerEvent::PARCEL_DELETE].event_enabled = 1; for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) { m_settings[i].retention_days = RETENTION_DAYS_DEFAULT; diff --git a/common/events/player_events.h b/common/events/player_events.h index def80a0ec..a7d5e0c10 100644 --- a/common/events/player_events.h +++ b/common/events/player_events.h @@ -58,6 +58,9 @@ namespace PlayerEvent { ITEM_CREATION, GUILD_TRIBUTE_DONATE_ITEM, GUILD_TRIBUTE_DONATE_PLAT, + PARCEL_SEND, + PARCEL_RETRIEVE, + PARCEL_DELETE, MAX // dont remove }; @@ -66,7 +69,7 @@ namespace PlayerEvent { // If event is unimplemented just tag (Unimplemented) in the name // Events don't get saved to the database if unimplemented or deprecated // Events tagged as deprecated will get automatically removed - static const char *EventName[PlayerEvent::MAX] = { + static const char *EventName[EventType::MAX] = { "None", "GM Command", "Zoning", @@ -116,7 +119,10 @@ namespace PlayerEvent { "Killed Raid NPC", "Item Creation", "Guild Tribute Donate Item", - "Guild Tribute Donate Platinum" + "Guild Tribute Donate Platinum", + "Parcel Item Sent", + "Parcel Item Retrieved", + "Parcel Prune Routine" }; // Generic struct used by all events @@ -976,6 +982,68 @@ namespace PlayerEvent { ); } }; + + struct ParcelRetrieve { + uint32 item_id; + uint32 quantity; + std::string from_player_name; + uint32 sent_date; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(quantity), + CEREAL_NVP(from_player_name), + CEREAL_NVP(sent_date) + ); + } + }; + + struct ParcelSend { + uint32 item_id; + uint32 quantity; + std::string from_player_name; + std::string to_player_name; + uint32 sent_date; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(quantity), + CEREAL_NVP(from_player_name), + CEREAL_NVP(to_player_name), + CEREAL_NVP(sent_date) + ); + } + }; + + struct ParcelDelete { + uint32 item_id; + uint32 quantity; + uint32 char_id; + std::string from_name; + std::string note; + uint32 sent_date; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(quantity), + CEREAL_NVP(char_id), + CEREAL_NVP(from_name), + CEREAL_NVP(note), + CEREAL_NVP(sent_date)); + } + }; } #endif //EQEMU_PLAYER_EVENTS_H diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index e036b9a2f..b9dfcedac 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -3089,21 +3089,6 @@ namespace RoF FINISH_ENCODE(); } - ENCODE(OP_ShopRequest) - { - ENCODE_LENGTH_EXACT(Merchant_Click_Struct); - SETUP_DIRECT_ENCODE(Merchant_Click_Struct, structs::Merchant_Click_Struct); - - OUT(npcid); - OUT(playerid); - OUT(command); - OUT(rate); - eq->unknown01 = 3; // Not sure what these values do yet, but list won't display without them - eq->unknown02 = 2592000; - - FINISH_ENCODE(); - } - ENCODE(OP_SkillUpdate) { ENCODE_LENGTH_EXACT(SkillUpdate_Struct); @@ -5047,19 +5032,6 @@ namespace RoF FINISH_DIRECT_DECODE(); } - DECODE(OP_ShopRequest) - { - DECODE_LENGTH_EXACT(structs::Merchant_Click_Struct); - SETUP_DIRECT_DECODE(Merchant_Click_Struct, structs::Merchant_Click_Struct); - - IN(npcid); - IN(playerid); - IN(command); - IN(rate); - - FINISH_DIRECT_DECODE(); - } - DECODE(OP_Trader) { uint32 psize = __packet->size; diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index f657ca842..052e66108 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -1579,30 +1579,75 @@ namespace RoF2 *p = nullptr; //store away the emu struct - uchar* __emu_buffer = in->pBuffer; + uchar *__emu_buffer = in->pBuffer; + ItemPacket_Struct *old_item_pkt = (ItemPacket_Struct *) __emu_buffer; - ItemPacket_Struct* old_item_pkt = (ItemPacket_Struct*)__emu_buffer; - EQ::InternalSerializedItem_Struct* int_struct = (EQ::InternalSerializedItem_Struct*)(&__emu_buffer[4]); + switch(old_item_pkt->PacketType) + { + case ItemPacketParcel: { + ParcelMessaging_Struct pms{}; + EQ::Util::MemoryStreamReader ss(reinterpret_cast(in->pBuffer), in->size); + cereal::BinaryInputArchive ar(ss); + ar(pms); - EQ::OutBuffer ob; - EQ::OutBuffer::pos_type last_pos = ob.tellp(); + uint32 player_name_length = pms.player_name.length(); + uint32 note_length = pms.note.length(); - ob.write((const char*)__emu_buffer, 4); + auto *int_struct = (EQ::InternalSerializedItem_Struct *) pms.serialized_item.data(); - SerializeItem(ob, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType); - if (ob.tellp() == last_pos) { - LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", int_struct->slot_id); - delete in; - return; - } + EQ::OutBuffer ob; + EQ::OutBuffer::pos_type last_pos = ob.tellp(); + ob.write(reinterpret_cast(&pms.packet_type), 4); - in->size = ob.size(); - in->pBuffer = ob.detach(); + SerializeItem(ob, (const EQ::ItemInstance *) int_struct->inst, pms.slot_id, 0, ItemPacketParcel); - delete[] __emu_buffer; + if (ob.tellp() == last_pos) { + LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", pms.slot_id); + safe_delete_array(__emu_buffer); + safe_delete(in); + return; + } - dest->FastQueuePacket(&in, ack_req); - } + ob.write((const char *) &pms.sent_time, 4); + ob.write((const char *) &player_name_length, 4); + ob.write(pms.player_name.c_str(), pms.player_name.length()); + ob.write((const char *) ¬e_length, 4); + ob.write(pms.note.c_str(), pms.note.length()); + + in->size = ob.size(); + in->pBuffer = ob.detach(); + + safe_delete_array(__emu_buffer); + dest->FastQueuePacket(&in, ack_req); + + break; + } + default: { + EQ::InternalSerializedItem_Struct *int_struct = (EQ::InternalSerializedItem_Struct *)(&__emu_buffer[4]); + + EQ::OutBuffer ob; + EQ::OutBuffer::pos_type last_pos = ob.tellp(); + + ob.write((const char *)__emu_buffer, 4); + + SerializeItem(ob, (const EQ::ItemInstance *)int_struct->inst, int_struct->slot_id, 0, + old_item_pkt->PacketType); + if (ob.tellp() == last_pos) { + LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", + int_struct->slot_id); + safe_delete_array(__emu_buffer); + safe_delete(in); + return; + } + + in->size = ob.size(); + in->pBuffer = ob.detach(); + + safe_delete_array(__emu_buffer); + dest->FastQueuePacket(&in, ack_req); + } + } + } ENCODE(OP_ItemVerifyReply) { @@ -3163,21 +3208,6 @@ namespace RoF2 FINISH_ENCODE(); } - ENCODE(OP_ShopRequest) - { - ENCODE_LENGTH_EXACT(Merchant_Click_Struct); - SETUP_DIRECT_ENCODE(Merchant_Click_Struct, structs::Merchant_Click_Struct); - - OUT(npcid); - OUT(playerid); - OUT(command); - OUT(rate); - eq->unknown01 = 3; // Not sure what these values do yet, but list won't display without them - eq->unknown02 = 2592000; - - FINISH_ENCODE(); - } - ENCODE(OP_SkillUpdate) { ENCODE_LENGTH_EXACT(SkillUpdate_Struct); @@ -5307,15 +5337,17 @@ namespace RoF2 FINISH_DIRECT_DECODE(); } - DECODE(OP_ShopRequest) + DECODE(OP_ShopSendParcel) { - DECODE_LENGTH_EXACT(structs::Merchant_Click_Struct); - SETUP_DIRECT_DECODE(Merchant_Click_Struct, structs::Merchant_Click_Struct); + DECODE_LENGTH_EXACT(structs::Parcel_Struct); + SETUP_DIRECT_DECODE(Parcel_Struct, structs::Parcel_Struct); - IN(npcid); - IN(playerid); - IN(command); - IN(rate); + IN(npc_id); + IN(quantity); + IN(money_flag); + emu->item_slot = RoF2ToServerTypelessSlot(eq->inventory_slot, invtype::typePossessions); + strn0cpy(emu->send_to, eq->send_to, sizeof(emu->send_to)); + strn0cpy(emu->note, eq->note, sizeof(emu->note)); FINISH_DIRECT_DECODE(); } @@ -5534,11 +5566,24 @@ namespace RoF2 //sprintf(hdr.unknown000, "06e0002Y1W00"); snprintf(hdr.unknown000, sizeof(hdr.unknown000), "%016d", item->ID); + if (packet_type == ItemPacketParcel) { + strn0cpy( + hdr.unknown000, + fmt::format( + "{:03}PAR{:010}\0", + inst->GetMerchantSlot(), + item->ID + ).c_str(), + sizeof(hdr.unknown000) + ); + } - hdr.stacksize = (inst->IsStackable() ? ((inst->GetCharges() > 1000) ? 0xFFFFFFFF : inst->GetCharges()) : 1); + hdr.stacksize = + item->ID == PARCEL_MONEY_ITEM_ID ? inst->GetPrice() : (inst->IsStackable() ? ((inst->GetCharges() > 1000) + ? 0xFFFFFFFF : inst->GetCharges()) : 1); hdr.unknown004 = 0; - structs::InventorySlot_Struct slot_id; + structs::InventorySlot_Struct slot_id{}; switch (packet_type) { case ItemPacketLoot: slot_id = ServerToRoF2CorpseSlot(slot_id_in); @@ -5548,22 +5593,24 @@ namespace RoF2 break; } - hdr.slot_type = (inst->GetMerchantSlot() ? invtype::typeMerchant : slot_id.Type); - hdr.main_slot = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : slot_id.Slot); - hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex); - hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex); - hdr.price = inst->GetPrice(); - hdr.merchant_slot = (inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1); - hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0); - hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber()); - hdr.unknown028 = 0; + hdr.slot_type = (inst->GetMerchantSlot() ? invtype::typeMerchant : slot_id.Type); + hdr.main_slot = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : slot_id.Slot); + hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex); + hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex); + hdr.price = inst->GetPrice(); + hdr.merchant_slot = ((inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1)); + hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0); + hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber()); + hdr.parcel_item_id = packet_type == ItemPacketParcel ? inst->GetID() : 0; hdr.last_cast_time = inst->GetRecastTimestamp(); - hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254) ? 0xFFFFFFFF : inst->GetCharges())); - hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0); - hdr.unknown044 = 0; - hdr.unknown048 = 0; - hdr.unknown052 = 0; - hdr.isEvolving = item->EvolvingItem; + hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254) + ? 0xFFFFFFFF + : inst->GetCharges())); + hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0); + hdr.unknown044 = 0; + hdr.unknown048 = 0; + hdr.unknown052 = 0; + hdr.isEvolving = item->EvolvingItem; ob.write((const char*)&hdr, sizeof(RoF2::structs::ItemSerializationHeader)); @@ -5621,9 +5668,10 @@ namespace RoF2 ob.write((const char*)&hdrf, sizeof(RoF2::structs::ItemSerializationHeaderFinish)); - if (strlen(item->Name) > 0) + if (strlen(item->Name) > 0) { ob.write(item->Name, strlen(item->Name)); - ob.write("\0", 1); + ob.write("\0", 1); + } if (strlen(item->Lore) > 0) ob.write(item->Lore, strlen(item->Lore)); @@ -5784,10 +5832,11 @@ namespace RoF2 itbs.unknown5 = 0; itbs.potion_belt_enabled = item->PotionBelt; - itbs.potion_belt_slots = item->PotionBeltSlots; - itbs.stacksize = (inst->IsStackable() ? item->StackSize : 0); - itbs.no_transfer = item->NoTransfer; - itbs.expendablearrow = item->ExpendableArrow; + itbs.potion_belt_slots = item->PotionBeltSlots; + itbs.stacksize = + item->ID == PARCEL_MONEY_ITEM_ID ? 0x7FFFFFFF : ((inst->IsStackable() ? item->StackSize : 0)); + itbs.no_transfer = item->NoTransfer; + itbs.expendablearrow = item->ExpendableArrow; // Done to hack older clients to label expendable fishing poles as such // July 28th, 2018 patch diff --git a/common/patches/rof2_ops.h b/common/patches/rof2_ops.h index 9c6c22349..7bc6c074a 100644 --- a/common/patches/rof2_ops.h +++ b/common/patches/rof2_ops.h @@ -116,7 +116,6 @@ E(OP_SendZonepoints) E(OP_SetGuildRank) E(OP_ShopPlayerBuy) E(OP_ShopPlayerSell) -E(OP_ShopRequest) E(OP_SkillUpdate) E(OP_SomeItemPacketMaybe) E(OP_SpawnAppearance) @@ -200,7 +199,7 @@ D(OP_Save) D(OP_SetServerFilter) D(OP_ShopPlayerBuy) D(OP_ShopPlayerSell) -D(OP_ShopRequest) +D(OP_ShopSendParcel) D(OP_Trader) D(OP_TraderBuy) D(OP_TradeSkillCombine) diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index 83ab33295..1063a79b7 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -2247,15 +2247,17 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; -/*008*/ uint32 command; // 1=open, 0=cancel/close -/*012*/ float rate; // cost multiplier, dosent work anymore -/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client -/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client -/*024*/ +struct MerchantClick_Struct +{ + /*000*/ uint32 npc_id; // Merchant NPC's entity id + /*004*/ uint32 player_id; + /*008*/ uint32 command; // 1=open, 0=cancel/close + /*012*/ float rate; // cost multiplier, dosent work anymore + /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels + /*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client + /*024*/ }; + /* Unknowns: 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE @@ -4572,25 +4574,25 @@ struct RoF2SlotStruct struct ItemSerializationHeader { -/*000*/ char unknown000[17]; // New for HoT. Looks like a string. -/*017*/ uint32 stacksize; -/*021*/ uint32 unknown004; -/*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ? -/*026*/ uint16 main_slot; -/*028*/ uint16 sub_slot; -/*030*/ uint16 aug_slot; // 0xffff -/*032*/ uint32 price; -/*036*/ uint32 merchant_slot; //1 if not a merchant item -/*040*/ uint32 scaled_value; //0 -/*044*/ uint32 instance_id; //unique instance id if not merchant item, else is merchant slot -/*048*/ uint32 unknown028; //0 -/*052*/ uint32 last_cast_time; // Unix Time from PP of last cast for this recast type if recast delay > 0 -/*056*/ uint32 charges; //Total Charges an item has (-1 for unlimited) -/*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items) -/*064*/ uint32 unknown044; // 0 -/*068*/ uint32 unknown048; // 0 -/*072*/ uint32 unknown052; // 0 - uint8 isEvolving; + /*000*/ char unknown000[17]; // New for HoT. Looks like a string. + /*017*/ uint32 stacksize; + /*021*/ uint32 unknown004; + /*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ? + /*026*/ uint16 main_slot; + /*028*/ uint16 sub_slot; + /*030*/ uint16 aug_slot; // 0xffff + /*032*/ uint32 price; + /*036*/ uint32 merchant_slot; // 1 if not a merchant item + /*040*/ uint32 scaled_value; // 0 + /*044*/ uint32 instance_id; // unique instance id if not merchant item, else is merchant slot + /*048*/ uint32 parcel_item_id; + /*052*/ uint32 last_cast_time; // Unix Time from PP of last cast for this recast type if recast delay > 0 + /*056*/ uint32 charges; // Total Charges an item has (-1 for unlimited) + /*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items) + /*064*/ uint32 unknown044; // 0 + /*068*/ uint32 unknown048; // 0 + /*072*/ uint32 unknown052; // 0 + uint8 isEvolving; }; struct EvolvingItem { @@ -5261,6 +5263,18 @@ struct Checksum_Struct { uint8_t data[2048]; }; +struct Parcel_Struct +{ + /*000*/ uint32 npc_id; + /*004*/ TypelessInventorySlot_Struct inventory_slot; + /*012*/ uint32 quantity; + /*016*/ uint32 money_flag; + /*020*/ char send_to[64]; + /*084*/ char note[128]; + /*212*/ uint32 unknown_212; + /*216*/ uint32 unknown_216; + /*220*/ uint32 unknown_220; +}; }; /*structs*/ }; /*RoF2*/ diff --git a/common/patches/rof_ops.h b/common/patches/rof_ops.h index 98f20f431..7ceb7644a 100644 --- a/common/patches/rof_ops.h +++ b/common/patches/rof_ops.h @@ -101,7 +101,6 @@ E(OP_SendZonepoints) E(OP_SetGuildRank) E(OP_ShopPlayerBuy) E(OP_ShopPlayerSell) -E(OP_ShopRequest) E(OP_SkillUpdate) E(OP_SomeItemPacketMaybe) E(OP_SpawnAppearance) @@ -183,7 +182,6 @@ D(OP_Save) D(OP_SetServerFilter) D(OP_ShopPlayerBuy) D(OP_ShopPlayerSell) -D(OP_ShopRequest) D(OP_Trader) D(OP_TraderBuy) D(OP_TradeSkillCombine) diff --git a/common/patches/rof_structs.h b/common/patches/rof_structs.h index 5f2b8e214..72ae67b1e 100644 --- a/common/patches/rof_structs.h +++ b/common/patches/rof_structs.h @@ -2200,15 +2200,17 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; -/*008*/ uint32 command; // 1=open, 0=cancel/close -/*012*/ float rate; // cost multiplier, dosent work anymore -/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client -/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client -/*024*/ +struct MerchantClick_Struct +{ + /*000*/ uint32 npc_id; // Merchant NPC's entity id + /*004*/ uint32 player_id; + /*008*/ uint32 command; // 1=open, 0=cancel/close + /*012*/ float rate; // cost multiplier, dosent work anymore + /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels + /*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client + /*024*/ }; + /* Unknowns: 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE diff --git a/common/patches/sod.cpp b/common/patches/sod.cpp index fd25d6f7e..6e15503d9 100644 --- a/common/patches/sod.cpp +++ b/common/patches/sod.cpp @@ -2026,6 +2026,19 @@ namespace SoD FINISH_ENCODE(); } + ENCODE(OP_ShopRequest) + { + ENCODE_LENGTH_EXACT(MerchantClick_Struct); + SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + OUT(npc_id); + OUT(player_id); + OUT(command); + OUT(rate); + + FINISH_ENCODE(); + } + ENCODE(OP_SomeItemPacketMaybe) { // This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow @@ -3483,6 +3496,21 @@ namespace SoD FINISH_DIRECT_DECODE(); } + DECODE(OP_ShopRequest) + { + DECODE_LENGTH_EXACT(structs::MerchantClick_Struct); + SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + IN(npc_id); + IN(player_id); + IN(command); + IN(rate); + emu->tab_display = 0; + emu->unknown020 = 0; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); diff --git a/common/patches/sod_ops.h b/common/patches/sod_ops.h index f314bb33f..bf9ce998a 100644 --- a/common/patches/sod_ops.h +++ b/common/patches/sod_ops.h @@ -78,6 +78,7 @@ E(OP_SendCharInfo) E(OP_SendZonepoints) E(OP_ShopPlayerBuy) E(OP_ShopPlayerSell) +E(OP_ShopRequest) E(OP_SomeItemPacketMaybe) E(OP_SpawnDoor) E(OP_SpecialMesg) @@ -141,6 +142,7 @@ D(OP_Save) D(OP_SetServerFilter) D(OP_ShopPlayerBuy) D(OP_ShopPlayerSell) +D(OP_ShopRequest) D(OP_TraderBuy) D(OP_TradeSkillCombine) D(OP_TributeItem) diff --git a/common/patches/sod_structs.h b/common/patches/sod_structs.h index 3a2586f58..7ec857208 100644 --- a/common/patches/sod_structs.h +++ b/common/patches/sod_structs.h @@ -1845,12 +1845,16 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; -/*008*/ uint32 command; //1=open, 0=cancel/close -/*012*/ float rate; //cost multiplier, dosent work anymore +struct MerchantClick_Struct +{ + /*000*/ uint32 npc_id; // Merchant NPC's entity id + /*004*/ uint32 player_id; + /*008*/ uint32 command; // 1=open, 0=cancel/close + /*012*/ float rate; // cost multiplier, dosent work anymore + /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels + /*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client }; + /* Unknowns: 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE diff --git a/common/patches/sof.cpp b/common/patches/sof.cpp index 15384813e..73abceb4a 100644 --- a/common/patches/sof.cpp +++ b/common/patches/sof.cpp @@ -1683,6 +1683,19 @@ namespace SoF FINISH_ENCODE(); } + ENCODE(OP_ShopRequest) + { + ENCODE_LENGTH_EXACT(MerchantClick_Struct); + SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + OUT(npc_id); + OUT(player_id); + OUT(command); + OUT(rate); + + FINISH_ENCODE(); + } + ENCODE(OP_SomeItemPacketMaybe) { // This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow @@ -2874,6 +2887,21 @@ namespace SoF FINISH_DIRECT_DECODE(); } + DECODE(OP_ShopRequest) + { + DECODE_LENGTH_EXACT(structs::MerchantClick_Struct); + SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + IN(npc_id); + IN(player_id); + IN(command); + IN(rate); + emu->tab_display = 0; + emu->unknown020 = 0; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); diff --git a/common/patches/sof_ops.h b/common/patches/sof_ops.h index 5100fcc26..57fd9c447 100644 --- a/common/patches/sof_ops.h +++ b/common/patches/sof_ops.h @@ -72,6 +72,7 @@ E(OP_SendAATable) E(OP_SendCharInfo) E(OP_SendZonepoints) E(OP_ShopPlayerSell) +E(OP_ShopRequest) E(OP_SomeItemPacketMaybe) E(OP_SpawnDoor) E(OP_SpecialMesg) @@ -128,6 +129,7 @@ D(OP_ReadBook) D(OP_Save) D(OP_SetServerFilter) D(OP_ShopPlayerSell) +D(OP_ShopRequest) D(OP_TraderBuy) D(OP_TradeSkillCombine) D(OP_TributeItem) diff --git a/common/patches/sof_structs.h b/common/patches/sof_structs.h index f6fe8ee69..c12527db5 100644 --- a/common/patches/sof_structs.h +++ b/common/patches/sof_structs.h @@ -1859,12 +1859,16 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; -/*008*/ uint32 command; //1=open, 0=cancel/close -/*012*/ float rate; //cost multiplier, dosent work anymore +struct MerchantClick_Struct +{ + /*000*/ uint32 npc_id; // Merchant NPC's entity id + /*004*/ uint32 player_id; + /*008*/ uint32 command; // 1=open, 0=cancel/close + /*012*/ float rate; // cost multiplier, dosent work anymore + /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels + /*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client }; + /* Unknowns: 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index 0674d1180..01b8edbdd 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -1858,6 +1858,19 @@ namespace Titanium FINISH_ENCODE(); } + ENCODE(OP_ShopRequest) + { + ENCODE_LENGTH_EXACT(MerchantClick_Struct); + SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + OUT(npc_id); + OUT(player_id); + OUT(command); + OUT(rate); + + FINISH_ENCODE(); + } + ENCODE(OP_SpecialMesg) { EQApplicationPacket *in = *p; @@ -2875,6 +2888,21 @@ namespace Titanium FINISH_DIRECT_DECODE(); } + DECODE(OP_ShopRequest) + { + DECODE_LENGTH_EXACT(structs::MerchantClick_Struct); + SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + IN(npc_id); + IN(player_id); + IN(command); + IN(rate); + emu->tab_display = 0; + emu->unknown020 = 0; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); diff --git a/common/patches/titanium_ops.h b/common/patches/titanium_ops.h index a57e42942..ce0de7a38 100644 --- a/common/patches/titanium_ops.h +++ b/common/patches/titanium_ops.h @@ -74,6 +74,7 @@ E(OP_SendCharInfo) E(OP_SendAATable) E(OP_SetFace) E(OP_ShopPlayerSell) +E(OP_ShopRequest) E(OP_SpawnAppearance) E(OP_SpecialMesg) E(OP_TaskDescription) @@ -120,6 +121,7 @@ D(OP_RaidInvite) D(OP_ReadBook) D(OP_SetServerFilter) D(OP_ShopPlayerSell) +D(OP_ShopRequest) D(OP_TraderBuy) D(OP_TradeSkillCombine) D(OP_TributeItem) diff --git a/common/patches/titanium_structs.h b/common/patches/titanium_structs.h index 61888c9df..7030dd565 100644 --- a/common/patches/titanium_structs.h +++ b/common/patches/titanium_structs.h @@ -1669,9 +1669,9 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; +struct MerchantClick_Struct { +/*000*/ uint32 npc_id; // Merchant NPC's entity id +/*004*/ uint32 player_id; /*008*/ uint32 command; //1=open, 0=cancel/close /*012*/ float rate; //cost multiplier, dosent work anymore }; diff --git a/common/patches/uf.cpp b/common/patches/uf.cpp index 3a78e0035..c77406500 100644 --- a/common/patches/uf.cpp +++ b/common/patches/uf.cpp @@ -2454,6 +2454,19 @@ namespace UF FINISH_ENCODE(); } + ENCODE(OP_ShopRequest) + { + ENCODE_LENGTH_EXACT(MerchantClick_Struct); + SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + OUT(npc_id); + OUT(player_id); + OUT(command); + OUT(rate); + + FINISH_ENCODE(); + } + ENCODE(OP_SomeItemPacketMaybe) { // This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow @@ -4047,6 +4060,21 @@ namespace UF FINISH_DIRECT_DECODE(); } + DECODE(OP_ShopRequest) + { + DECODE_LENGTH_EXACT(structs::MerchantClick_Struct); + SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct); + + IN(npc_id); + IN(player_id); + IN(command); + IN(rate); + emu->tab_display = 0; + emu->unknown020 = 0; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_TraderBuy) { DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); diff --git a/common/patches/uf_ops.h b/common/patches/uf_ops.h index 431456855..2ed03364f 100644 --- a/common/patches/uf_ops.h +++ b/common/patches/uf_ops.h @@ -89,6 +89,7 @@ E(OP_SendZonepoints) E(OP_SetGuildRank) E(OP_ShopPlayerBuy) E(OP_ShopPlayerSell) +E(OP_ShopRequest) E(OP_SomeItemPacketMaybe) E(OP_SpawnAppearance) E(OP_SpawnDoor) @@ -158,6 +159,7 @@ D(OP_Save) D(OP_SetServerFilter) D(OP_ShopPlayerBuy) D(OP_ShopPlayerSell) +D(OP_ShopRequest) D(OP_TraderBuy) D(OP_TradeSkillCombine) D(OP_TributeItem) diff --git a/common/patches/uf_structs.h b/common/patches/uf_structs.h index 8c7dae5b5..1f0ce2527 100644 --- a/common/patches/uf_structs.h +++ b/common/patches/uf_structs.h @@ -1916,9 +1916,9 @@ struct TimeOfDay_Struct { }; // Darvik: shopkeeper structs -struct Merchant_Click_Struct { -/*000*/ uint32 npcid; // Merchant NPC's entity id -/*004*/ uint32 playerid; +struct MerchantClick_Struct { +/*000*/ uint32 npc_id; // Merchant NPC's entity id +/*004*/ uint32 player_id; /*008*/ uint32 command; //1=open, 0=cancel/close /*012*/ float rate; //cost multiplier, dosent work anymore }; diff --git a/common/repositories/base/base_character_parcels_repository.h b/common/repositories/base/base_character_parcels_repository.h new file mode 100644 index 000000000..8bb6c6b3c --- /dev/null +++ b/common/repositories/base/base_character_parcels_repository.h @@ -0,0 +1,463 @@ +/** + * 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_PARCELS_REPOSITORY_H +#define EQEMU_BASE_CHARACTER_PARCELS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseCharacterParcelsRepository { +public: + struct CharacterParcels { + uint32_t id; + uint32_t char_id; + uint32_t item_id; + uint32_t slot_id; + uint32_t quantity; + std::string from_name; + std::string note; + time_t sent_date; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "char_id", + "item_id", + "slot_id", + "quantity", + "from_name", + "note", + "sent_date", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "char_id", + "item_id", + "slot_id", + "quantity", + "from_name", + "note", + "UNIX_TIMESTAMP(sent_date)", + }; + } + + 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_parcels"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static CharacterParcels NewEntity() + { + CharacterParcels e{}; + + e.id = 0; + e.char_id = 0; + e.item_id = 0; + e.slot_id = 0; + e.quantity = 0; + e.from_name = ""; + e.note = ""; + e.sent_date = 0; + + return e; + } + + static CharacterParcels GetCharacterParcels( + const std::vector &character_parcelss, + int character_parcels_id + ) + { + for (auto &character_parcels : character_parcelss) { + if (character_parcels.id == character_parcels_id) { + return character_parcels; + } + } + + return NewEntity(); + } + + static CharacterParcels FindOne( + Database& db, + int character_parcels_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + character_parcels_id + ) + ); + + auto row = results.begin(); + 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.slot_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.quantity = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.from_name = row[5] ? row[5] : ""; + e.note = row[6] ? row[6] : ""; + e.sent_date = strtoll(row[7] ? row[7] : "-1", nullptr, 10); + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int character_parcels_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + character_parcels_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const CharacterParcels &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.char_id)); + v.push_back(columns[2] + " = " + std::to_string(e.item_id)); + v.push_back(columns[3] + " = " + std::to_string(e.slot_id)); + v.push_back(columns[4] + " = " + std::to_string(e.quantity)); + v.push_back(columns[5] + " = '" + Strings::Escape(e.from_name) + "'"); + v.push_back(columns[6] + " = '" + Strings::Escape(e.note) + "'"); + v.push_back(columns[7] + " = FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static CharacterParcels InsertOne( + Database& db, + CharacterParcels e + ) + { + 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.item_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.quantity)); + v.push_back("'" + Strings::Escape(e.from_name) + "'"); + v.push_back("'" + Strings::Escape(e.note) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + + 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.char_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.quantity)); + v.push_back("'" + Strings::Escape(e.from_name) + "'"); + v.push_back("'" + Strings::Escape(e.note) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + + 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) { + 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.slot_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.quantity = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.from_name = row[5] ? row[5] : ""; + e.note = row[6] ? row[6] : ""; + e.sent_date = strtoll(row[7] ? row[7] : "-1", nullptr, 10); + + 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) { + 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.slot_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.quantity = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.from_name = row[5] ? row[5] : ""; + e.note = row[6] ? row[6] : ""; + e.sent_date = strtoll(row[7] ? row[7] : "-1", nullptr, 10); + + 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 CharacterParcels &e + ) + { + 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.item_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.quantity)); + v.push_back("'" + Strings::Escape(e.from_name) + "'"); + v.push_back("'" + Strings::Escape(e.note) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + + 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.char_id)); + v.push_back(std::to_string(e.item_id)); + v.push_back(std::to_string(e.slot_id)); + v.push_back(std::to_string(e.quantity)); + v.push_back("'" + Strings::Escape(e.from_name) + "'"); + v.push_back("'" + Strings::Escape(e.note) + "'"); + v.push_back("FROM_UNIXTIME(" + (e.sent_date > 0 ? std::to_string(e.sent_date) : "null") + ")"); + + 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_PARCELS_REPOSITORY_H diff --git a/common/repositories/base/base_npc_types_repository.h b/common/repositories/base/base_npc_types_repository.h index 929e3248f..1f0720d92 100644 --- a/common/repositories/base/base_npc_types_repository.h +++ b/common/repositories/base/base_npc_types_repository.h @@ -146,6 +146,7 @@ public: int32_t heroic_strikethrough; int32_t faction_amount; uint8_t keeps_sold_items; + uint8_t is_parcel_merchant; }; static std::string PrimaryKey() @@ -283,6 +284,7 @@ public: "heroic_strikethrough", "faction_amount", "keeps_sold_items", + "is_parcel_merchant", }; } @@ -416,6 +418,7 @@ public: "heroic_strikethrough", "faction_amount", "keeps_sold_items", + "is_parcel_merchant", }; } @@ -583,6 +586,7 @@ public: e.heroic_strikethrough = 0; e.faction_amount = 0; e.keeps_sold_items = 1; + e.is_parcel_merchant = 0; return e; } @@ -746,6 +750,7 @@ public: e.heroic_strikethrough = row[124] ? static_cast(atoi(row[124])) : 0; e.faction_amount = row[125] ? static_cast(atoi(row[125])) : 0; e.keeps_sold_items = row[126] ? static_cast(strtoul(row[126], nullptr, 10)) : 1; + e.is_parcel_merchant = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 0; return e; } @@ -905,6 +910,7 @@ public: v.push_back(columns[124] + " = " + std::to_string(e.heroic_strikethrough)); v.push_back(columns[125] + " = " + std::to_string(e.faction_amount)); v.push_back(columns[126] + " = " + std::to_string(e.keeps_sold_items)); + v.push_back(columns[127] + " = " + std::to_string(e.is_parcel_merchant)); auto results = db.QueryDatabase( fmt::format( @@ -1053,6 +1059,7 @@ public: v.push_back(std::to_string(e.heroic_strikethrough)); v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); + v.push_back(std::to_string(e.is_parcel_merchant)); auto results = db.QueryDatabase( fmt::format( @@ -1209,6 +1216,7 @@ public: v.push_back(std::to_string(e.heroic_strikethrough)); v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); + v.push_back(std::to_string(e.is_parcel_merchant)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -1369,6 +1377,7 @@ public: e.heroic_strikethrough = row[124] ? static_cast(atoi(row[124])) : 0; e.faction_amount = row[125] ? static_cast(atoi(row[125])) : 0; e.keeps_sold_items = row[126] ? static_cast(strtoul(row[126], nullptr, 10)) : 1; + e.is_parcel_merchant = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1520,6 +1529,7 @@ public: e.heroic_strikethrough = row[124] ? static_cast(atoi(row[124])) : 0; e.faction_amount = row[125] ? static_cast(atoi(row[125])) : 0; e.keeps_sold_items = row[126] ? static_cast(strtoul(row[126], nullptr, 10)) : 1; + e.is_parcel_merchant = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1721,6 +1731,7 @@ public: v.push_back(std::to_string(e.heroic_strikethrough)); v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); + v.push_back(std::to_string(e.is_parcel_merchant)); auto results = db.QueryDatabase( fmt::format( @@ -1870,6 +1881,7 @@ public: v.push_back(std::to_string(e.heroic_strikethrough)); v.push_back(std::to_string(e.faction_amount)); v.push_back(std::to_string(e.keeps_sold_items)); + v.push_back(std::to_string(e.is_parcel_merchant)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/character_parcels_repository.h b/common/repositories/character_parcels_repository.h new file mode 100644 index 000000000..4eba7e0db --- /dev/null +++ b/common/repositories/character_parcels_repository.h @@ -0,0 +1,83 @@ +#ifndef EQEMU_CHARACTER_PARCELS_REPOSITORY_H +#define EQEMU_CHARACTER_PARCELS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_character_parcels_repository.h" + +class CharacterParcelsRepository: public BaseCharacterParcelsRepository { +public: + + /** + * 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 + * + * ParcelsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * ParcelsRepository::GetWhereNeverExpires() + * ParcelsRepository::GetWhereXAndY() + * ParcelsRepository::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 + struct ParcelCountAndCharacterName + { + std::string character_name; + uint32 char_id; + uint32 parcel_count; + }; + + static std::vector GetParcelCountAndCharacterName(Database &db, const std::string &character_name) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "SELECT c.name, COUNT(p.id), c.id FROM character_data c " + "JOIN character_parcels p ON p.char_id = c.id " + "WHERE c.name = '{}' " + "LIMIT 1", + character_name) + ); + + all_entries.reserve(results.RowCount()); + + for(auto row = results.begin(); row != results.end(); ++row) { + ParcelCountAndCharacterName e {}; + + e.character_name = row[0] ? row[0] : ""; + e.parcel_count = row[1] ? Strings::ToUnsignedInt(row[1]) : 0; + e.char_id = row[2] ? Strings::ToUnsignedInt(row[2]) : 0; + + all_entries.push_back(e); + } + + return all_entries; + } +}; + +#endif //EQEMU_CHARACTER_PARCELS_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 2eb1821b2..d6ba73ca0 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -959,6 +959,16 @@ RULE_BOOL(Items, DisablePotionBelt, false, "Enable this to disable Potion Belt I RULE_BOOL(Items, DisableSpellFocusEffects, false, "Enable this to disable Spell Focus Effects on Items") RULE_CATEGORY_END() +RULE_CATEGORY(Parcel) +RULE_BOOL(Parcel, EnableParcelMerchants, true, "Enable or Disable Parcel Merchants. Requires RoF+ Clients.") +RULE_BOOL(Parcel, EnableDirectToInventoryDelivery, false, "Enable or Disable RoF2 bazaar purchases to be delivered directly to the buyer's inventory.") +RULE_BOOL(Parcel, DeleteOnDuplicate, false, "Delete retrieved item if it creates a lore conflict.") +RULE_BOOL(Parcel, EnablePruning, false, "Enable the automatic pruning of sent parcels. Uses rule ParcelPruneDelay for prune delay.") +RULE_INT(Parcel, ParcelDeliveryDelay, 30000, "Sets the time that a player must wait between sending parcels.") +RULE_INT(Parcel, ParcelMaxItems, 50, "The maximum number of parcels a player is allowed to have in their mailbox.") +RULE_INT(Parcel, ParcelPruneDelay, 30, "The number of days after which a parcel is deleted. Items are lost!") +RULE_CATEGORY_END() + #undef RULE_CATEGORY #undef RULE_INT #undef RULE_REAL diff --git a/common/server_event_scheduler.h b/common/server_event_scheduler.h index 5671eaf7c..a198e73fa 100644 --- a/common/server_event_scheduler.h +++ b/common/server_event_scheduler.h @@ -11,7 +11,7 @@ namespace ServerEvents { static const std::string EVENT_TYPE_RELOAD_WORLD = "reload_world"; static const std::string EVENT_TYPE_RULE_CHANGE = "rule_change"; static const std::string EVENT_TYPE_CONTENT_FLAG_CHANGE = "content_flag_change"; -} +} // namespace ServerEvents class ServerEventScheduler { public: diff --git a/common/servertalk.h b/common/servertalk.h index 2a8621a70..6fb03a987 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -113,6 +113,9 @@ #define ServerOP_GuildSendGuildList 0x007E #define ServerOP_GuildMembersList 0x007F +#define ServerOP_ParcelDelivery 0x0090 +#define ServerOP_ParcelPrune 0x0091 + #define ServerOP_RaidAdd 0x0100 //in use #define ServerOP_RaidRemove 0x0101 //in use #define ServerOP_RaidDisband 0x0102 //in use diff --git a/common/version.h b/common/version.h index 9bb12e332..3f885bfdc 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9270 +#define CURRENT_BINARY_DATABASE_VERSION 9271 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9043 #endif diff --git a/shared_memory/main.cpp b/shared_memory/main.cpp index b9293fbe1..c598b39f5 100644 --- a/shared_memory/main.cpp +++ b/shared_memory/main.cpp @@ -32,11 +32,13 @@ #include "../common/content/world_content_service.h" #include "../common/zone_store.h" #include "../common/path_manager.h" +#include "../common/events/player_event_logs.h" -EQEmuLogSys LogSys; +EQEmuLogSys LogSys; WorldContentService content_service; -ZoneStore zone_store; -PathManager path; +ZoneStore zone_store; +PathManager path; +PlayerEventLogs player_event_logs; #ifdef _WINDOWS #include diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index f142cb699..c969ced09 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -467,6 +467,10 @@ OP_ShopEnd=0x30a8 OP_ShopEndConfirm=0x3196 OP_ShopPlayerBuy=0x0ddd OP_ShopDelItem=0x724f +OP_ShopSendParcel=0x3a5d +OP_ShopDeleteParcel=0x47f1 +OP_ShopRetrieveParcel=0x7013 +OP_ShopParcelIcon=0x46f0 # tradeskill stuff: OP_ClickObject=0x4aa1 diff --git a/utils/sql/git/optional/2024_04_20_Parcel_Merchant_Updates.sql b/utils/sql/git/optional/2024_04_20_Parcel_Merchant_Updates.sql new file mode 100644 index 000000000..60216ad33 --- /dev/null +++ b/utils/sql/git/optional/2024_04_20_Parcel_Merchant_Updates.sql @@ -0,0 +1,3 @@ +UPDATE `npc_types` SET `is_parcel_merchant` = 1, `lastname` = 'Parcels and General Supplies' +WHERE id IN (202129, 3036, 394025, 75113, 49073, 41021, 40070, 106115, 55150, 9053, 382156, 1032, +155088, 23017, 61065, 29008, 67058, 54067, 19031, 50140); diff --git a/world/main.cpp b/world/main.cpp index 1ba5bbfa6..aff459fb9 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -87,6 +87,7 @@ #include "../common/path_manager.h" #include "../common/events/player_event_logs.h" #include "../common/skill_caps.h" +#include "../common/repositories/character_parcels_repository.h" SkillCaps skill_caps; ZoneStore zone_store; @@ -176,6 +177,9 @@ int main(int argc, char **argv) PurgeInstanceTimer.Start(450000); Timer EQTimeTimer(600000); EQTimeTimer.Start(600000); + Timer parcel_prune_timer(86400000); + parcel_prune_timer.Start(86400000); + // global loads LogInfo("Loading launcher list"); @@ -420,6 +424,20 @@ int main(int argc, char **argv) client_list.Process(); guild_mgr.Process(); + if (parcel_prune_timer.Check()) { + if (RuleB(Parcel, EnableParcelMerchants) && RuleB(Parcel, EnablePruning)) { + LogTrading( + "Parcel Prune process running for parcels over [{}] days", + RuleI(Parcel, ParcelPruneDelay) + ); + + auto out = std::make_unique(ServerOP_ParcelPrune); + zoneserver_list.SendPacketToBootedZones(out.get()); + + database.PurgeCharacterParcels(); + } + } + if (player_event_process_timer.Check()) { player_event_logs.Process(); } diff --git a/world/world_event_scheduler.cpp b/world/world_event_scheduler.cpp index a38c4684f..86342b882 100644 --- a/world/world_event_scheduler.cpp +++ b/world/world_event_scheduler.cpp @@ -1,6 +1,7 @@ #include "world_event_scheduler.h" #include "../common/servertalk.h" #include +#include "../common/rulesys.h" void WorldEventScheduler::Process(ZSList *zs_list) { @@ -31,13 +32,10 @@ void WorldEventScheduler::Process(ZSList *zs_list) ); for (auto &e: m_events) { - // discard uninteresting events as its less work to calculate time on events we don't care about // different processes are interested in different events - if ( - e.event_type != ServerEvents::EVENT_TYPE_BROADCAST && - e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD - ) { + if (e.event_type != ServerEvents::EVENT_TYPE_BROADCAST && + e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD) { continue; } @@ -57,7 +55,7 @@ void WorldEventScheduler::Process(ZSList *zs_list) if (e.event_type == ServerEvents::EVENT_TYPE_RELOAD_WORLD) { LogScheduler("Sending reload world event [{}]", e.event_data.c_str()); - auto pack = new ServerPacket(ServerOP_ReloadWorld, sizeof(ReloadWorld_Struct)); + auto pack = new ServerPacket(ServerOP_ReloadWorld, sizeof(ReloadWorld_Struct)); auto *reload_world = (ReloadWorld_Struct *) pack->pBuffer; reload_world->global_repop = ReloadWorld::Repop; zs_list->SendPacket(pack); diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index ce8b0fcc1..cf3fe0014 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1730,6 +1730,19 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { } break; } + case ServerOP_ParcelDelivery: { + auto in = (Parcel_Struct *) pack->pBuffer; + if (strlen(in->send_to) == 0) { + LogError( + "ServerOP_ParcelDelivery pack received with invalid character name of [{}]", + in->send_to); + return; + } + + zoneserver_list.SendPacketToBootedZones(pack); + + break; + } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); DumpPacket(pack->pBuffer, pack->size); diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 662c35e0f..ddfa57d15 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -101,6 +101,7 @@ SET(zone_sources npc_scale_manager.cpp object.cpp oriented_bounding_box.cpp + parcels.cpp pathfinder_interface.cpp pathfinder_nav_mesh.cpp pathfinder_null.cpp diff --git a/zone/client.cpp b/zone/client.cpp index ee9f3e8de..40653d41a 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -184,7 +184,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob( mob_close_scan_timer(6000), position_update_timer(10000), consent_throttle_timer(2000), - tmSitting(0) + tmSitting(0), + parcel_timer(RuleI(Parcel, ParcelDeliveryDelay)) { for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) { SetFilter(client_filter, FilterShow); @@ -375,6 +376,15 @@ Client::Client(EQStreamInterface *ieqs) : Mob( bot_owner_options[booBuffCounter] = false; bot_owner_options[booMonkWuMessage] = false; + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + m_parcel_count = 0; + m_parcel_enabled = true; + m_parcel_merchant_engaged = false; + m_parcels.clear(); + SetBotPulling(false); SetBotPrecombat(false); @@ -383,6 +393,10 @@ Client::Client(EQStreamInterface *ieqs) : Mob( } Client::~Client() { + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + } + mMovementManager->RemoveClient(this); DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); diff --git a/zone/client.h b/zone/client.h index b16e5d55e..e73a826eb 100644 --- a/zone/client.h +++ b/zone/client.h @@ -68,6 +68,7 @@ namespace EQ #include "cheat_manager.h" #include "../common/events/player_events.h" #include "../common/data_verification.h" +#include "../common/repositories/character_parcels_repository.h" #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) @@ -323,8 +324,36 @@ public: 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 FinishTrade(Mob* with, bool finalizer = false, void* event_entry = nullptr, std::list* event_details = nullptr); + void FinishTrade( + Mob *with, + bool finalizer = false, + void *event_entry = nullptr, + std::list *event_details = nullptr + ); void SendZonePoints(); + void SendBulkParcels(); + void DoParcelCancel(); + void DoParcelSend(const Parcel_Struct *parcel_in); + void DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in); + void SendParcel(const Parcel_Struct &parcel); + void SendParcelStatus(); + void SendParcelAck(); + void SendParcelRetrieveAck(); + void SendParcelDelete(const ParcelRetrieve_Struct &parcel_in); + void SendParcelDeliveryToWorld(const Parcel_Struct &parcel); + void SetParcelEnabled(bool status) { m_parcel_enabled = status; } + bool GetParcelEnabled() { return m_parcel_enabled; } + void SetParcelCount(uint32 count) { m_parcel_count = count; } + int32 GetParcelCount() { return m_parcel_count; } + bool GetEngagedWithParcelMerchant() { return m_parcel_merchant_engaged; } + void SetEngagedWithParcelMerchant(bool status) { m_parcel_merchant_engaged = status; } + Timer *GetParcelTimer() { return &parcel_timer; } + bool DeleteParcel(uint32 parcel_id); + void AddParcel(CharacterParcelsRepository::CharacterParcels &parcel); + void LoadParcels(); + std::map GetParcels() { return m_parcels; } + int32 FindNextFreeParcelSlot(uint32 char_id); + void SendParcelIconStatus(); void SendBuyerResults(char *SearchQuery, uint32 SearchID); void ShowBuyLines(const EQApplicationPacket *app); @@ -1857,6 +1886,14 @@ private: bool Trader; bool Buyer; std::string BuyerWelcomeMessage; + int32 m_parcel_platinum; + int32 m_parcel_gold; + int32 m_parcel_silver; + int32 m_parcel_copper; + int32 m_parcel_count; + bool m_parcel_enabled; + bool m_parcel_merchant_engaged; + std::map m_parcels{}; int Haste; //precalced value uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004 @@ -1955,6 +1992,7 @@ private: Timer dynamiczone_removal_timer; Timer task_request_timer; Timer pick_lock_timer; + Timer parcel_timer; //Used to limit the number of parcels to one every 30 seconds (default). Changable via rule. glm::vec3 m_Proximity; glm::vec4 last_position_before_bulk_update; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 6a95dae5e..806dc11c6 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -69,6 +69,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/events/player_event_logs.h" #include "../common/repositories/character_stats_record_repository.h" #include "dialogue_window.h" +#include "../common/rulesys.h" extern QueryServ* QServ; extern Zone* zone; @@ -321,6 +322,8 @@ void MapOpcodes() ConnectedOpcodes[OP_OpenGuildTributeMaster] = &Client::Handle_OP_OpenGuildTributeMaster; ConnectedOpcodes[OP_OpenInventory] = &Client::Handle_OP_OpenInventory; ConnectedOpcodes[OP_OpenTributeMaster] = &Client::Handle_OP_OpenTributeMaster; + ConnectedOpcodes[OP_ShopSendParcel] = &Client::Handle_OP_ShopSendParcel; + ConnectedOpcodes[OP_ShopRetrieveParcel] = &Client::Handle_OP_ShopRetrieveParcel; ConnectedOpcodes[OP_PDeletePetition] = &Client::Handle_OP_PDeletePetition; ConnectedOpcodes[OP_PetCommands] = &Client::Handle_OP_PetCommands; ConnectedOpcodes[OP_Petition] = &Client::Handle_OP_Petition; @@ -818,6 +821,10 @@ void Client::CompleteConnect() ); } + if(ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { + SendParcelStatus(); + } + if (zone && zone->GetInstanceTimer()) { bool is_permanent = false; uint32 remaining_time = database.GetTimeRemainingInstance(zone->GetInstanceID(), is_permanent); @@ -4271,6 +4278,12 @@ void Client::Handle_OP_Bug(const EQApplicationPacket *app) void Client::Handle_OP_Camp(const EQApplicationPacket *app) { + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants) && GetEngagedWithParcelMerchant()) { + Stand(); + MessageString(Chat::Yellow, TRADER_BUSY_TWO); + return; + } + if (IsLFP()) worldserver.StopLFP(CharacterID()); @@ -4324,6 +4337,12 @@ void Client::Handle_OP_CancelTrade(const EQApplicationPacket *app) FinishTrade(this); trade->Reset(); } + + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + SetEngagedWithParcelMerchant(false); + } + EQApplicationPacket end_trade1(OP_FinishWindow, 0); QueuePacket(&end_trade1); @@ -13877,6 +13896,11 @@ void Client::Handle_OP_Shielding(const EQApplicationPacket *app) void Client::Handle_OP_ShopEnd(const EQApplicationPacket *app) { + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) { + DoParcelCancel(); + SetEngagedWithParcelMerchant(false); + } + EQApplicationPacket empty(OP_ShopEndConfirm); QueuePacket(&empty); return; @@ -14423,82 +14447,107 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app) { - if (app->size != sizeof(Merchant_Click_Struct)) { - LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(Merchant_Click_Struct)); + if (app->size != sizeof(MerchantClick_Struct)) { + LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(MerchantClick_Struct)); return; } - Merchant_Click_Struct* mc = (Merchant_Click_Struct*)app->pBuffer; + MerchantClick_Struct *mc = (MerchantClick_Struct *) app->pBuffer; // Send back opcode OP_ShopRequest - tells client to open merchant window. - //EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); - //Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer; - int merchantid = 0; - Mob* tmp = entity_list.GetMob(mc->npcid); + // EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); + // Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer; + int merchant_id = 0; + int tabs_to_display = None; + Mob *tmp = entity_list.GetMob(mc->npc_id); - if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant) + if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant) { return; + } - //you have to be somewhat close to them to be properly using them - if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) + // you have to be somewhat close to them to be properly using them + if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) { return; + } - merchantid = tmp->CastToNPC()->MerchantType; + merchant_id = tmp->CastToNPC()->MerchantType; + + if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && tmp->CastToNPC()->GetParcelMerchant()) { + tabs_to_display = SellBuyParcel; + } + else { + tabs_to_display = SellBuy; + } + + int action = MerchantActions::Open; + if (merchant_id == 0) { + auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(MerchantClick_Struct)); + auto mco = (MerchantClick_Struct *) outapp->pBuffer; + mco->npc_id = mc->npc_id; + mco->player_id = 0; + mco->command = MerchantActions::Open; + mco->rate = 1.0; + mco->tab_display = tabs_to_display; - int action = 1; - if (merchantid == 0) { - auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); - Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer; - mco->npcid = mc->npcid; - mco->playerid = 0; - mco->command = 1; //open... - mco->rate = 1.0; QueuePacket(outapp); safe_delete(outapp); return; } + if (tmp->IsEngaged()) { MessageString(Chat::White, MERCHANT_BUSY); - action = 0; - } - if (GetFeigned() || IsInvisible()) - { - Message(Chat::White, "You cannot use a merchant right now."); - action = 0; - } - int primaryfaction = tmp->CastToNPC()->GetPrimaryFaction(); - int factionlvl = GetFactionLevel(CharacterID(), tmp->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), primaryfaction, tmp); - if (factionlvl >= 7) { - MerchantRejectMessage(tmp, primaryfaction); - action = 0; + action = MerchantActions::Close; } - if (tmp->Charmed()) - action = 0; + if (GetFeigned() || IsInvisible()) { + Message(Chat::White, "You cannot use a merchant right now."); + action = MerchantActions::Close; + } + + int primaryfaction = tmp->CastToNPC()->GetPrimaryFaction(); + int factionlvl = GetFactionLevel( + CharacterID(), tmp->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), + primaryfaction, tmp + ); + if (factionlvl >= 7) { + MerchantRejectMessage(tmp, primaryfaction); + action = MerchantActions::Close; + } + + if (tmp->Charmed()) { + action = MerchantActions::Close; + } if (!tmp->CastToNPC()->IsMerchantOpen()) { tmp->SayString(zone->random.Int(MERCHANT_CLOSED_ONE, MERCHANT_CLOSED_THREE)); - action = 0; + action = MerchantActions::Close; } - auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); - Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer; + auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(MerchantClick_Struct)); + auto mco = (MerchantClick_Struct *) outapp->pBuffer; + + mco->npc_id = mc->npc_id; + mco->player_id = 0; + mco->command = action; // Merchant command 0x01 = open + mco->tab_display = tabs_to_display; - mco->npcid = mc->npcid; - mco->playerid = 0; - mco->command = action; // Merchant command 0x01 = open if (RuleB(Merchant, UsePriceMod)) { - mco->rate = 1 / ((RuleR(Merchant, BuyCostMod))*Client::CalcPriceMod(tmp, true)); // works + mco->rate = 1 / ((RuleR(Merchant, BuyCostMod)) * Client::CalcPriceMod(tmp, true)); // works } - else + else { mco->rate = 1 / (RuleR(Merchant, BuyCostMod)); + } outapp->priority = 6; QueuePacket(outapp); safe_delete(outapp); - if (action == 1) - BulkSendMerchantInventory(merchantid, tmp->GetNPCTypeID()); + if (action == MerchantActions::Open) { + BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID()); + if ((tabs_to_display & Parcel) == Parcel) { + SendBulkParcels(); + } + } return; } @@ -17162,3 +17211,26 @@ void Client::Handle_OP_GuildTributeDonatePlat(const EQApplicationPacket *app) } } +void Client::Handle_OP_ShopSendParcel(const EQApplicationPacket *app) +{ + if (app->size != sizeof(Parcel_Struct)) { + LogError("Received Handle_OP_ShopSendParcel packet. Expected size {}, received size {}.", sizeof(Parcel_Struct), + app->size); + return; + } + + auto parcel_in = (Parcel_Struct *)app->pBuffer; + DoParcelSend(parcel_in); +} + +void Client::Handle_OP_ShopRetrieveParcel(const EQApplicationPacket *app) +{ + if (app->size != sizeof(ParcelRetrieve_Struct)) { + LogError("Received Handle_OP_ShopRetrieveParcel packet. Expected size {}, received size {}.", + sizeof(ParcelRetrieve_Struct), app->size); + return; + } + + auto parcel_in = (ParcelRetrieve_Struct *)app->pBuffer; + DoParcelRetrieve(*parcel_in); +} diff --git a/zone/client_packet.h b/zone/client_packet.h index bb84b3c00..57a4bfcf4 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -335,3 +335,6 @@ void Handle_OP_SharedTaskAccept(const EQApplicationPacket *app); void Handle_OP_SharedTaskQuit(const EQApplicationPacket *app); void Handle_OP_SharedTaskPlayerList(const EQApplicationPacket *app); + + void Handle_OP_ShopSendParcel(const EQApplicationPacket *app); + void Handle_OP_ShopRetrieveParcel(const EQApplicationPacket *app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 513f615b7..828101b7a 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -555,6 +555,7 @@ bool Client::Process() { guild_mgr.UpdateDbMemberOnline(CharacterID(), false); guild_mgr.SendToWorldSendGuildMembersList(GuildID()); } + return false; } else if (!linkdead_timer.Enabled()) { @@ -1429,6 +1430,22 @@ void Client::OPMoveCoin(const EQApplicationPacket* app) to_bucket = (int32 *) &trade->cp; break; } } + else { + switch (mc->cointype2) { + case COINTYPE_PP: + m_parcel_platinum += mc->amount; + break; + case COINTYPE_GP: + m_parcel_gold += mc->amount; + break; + case COINTYPE_SP: + m_parcel_silver += mc->amount; + break; + case COINTYPE_CP: + m_parcel_copper += mc->amount; + break; + } + } break; } case 4: // shared bank diff --git a/zone/command.cpp b/zone/command.cpp index 08e040318..b7af495f9 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -182,6 +182,7 @@ int command_init(void) command_add("nukeitem", "[Item ID] - Removes the specified Item ID from you or your player target's inventory", AccountStatus::GMLeadAdmin, command_nukeitem) || command_add("object", "List|Add|Edit|Move|Rotate|Copy|Save|Undo|Delete - Manipulate static and tradeskill objects within the zone", AccountStatus::GMAdmin, command_object) || command_add("opcode", "Reloads all opcodes from server patch files", AccountStatus::GMMgmt, command_reload) || + command_add("parcels", "View and edit the parcel system. Requires parcels to be enabled in rules.", AccountStatus::GMMgmt, command_parcels) || command_add("path", "view and edit pathing", AccountStatus::GMMgmt, command_path) || command_add("peqzone", "[Zone ID|Zone Short Name] - Teleports you to the specified zone if you meet the requirements.", AccountStatus::Player, command_peqzone) || command_add("petitems", "View your pet's items if you have one", AccountStatus::ApprenticeGuide, command_petitems) || @@ -873,6 +874,7 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/nukeitem.cpp" #include "gm_commands/object.cpp" #include "gm_commands/object_manipulation.cpp" +#include "gm_commands/parcels.cpp" #include "gm_commands/path.cpp" #include "gm_commands/peqzone.cpp" #include "gm_commands/petitems.cpp" diff --git a/zone/command.h b/zone/command.h index 141c37d92..ae8bf30c6 100644 --- a/zone/command.h +++ b/zone/command.h @@ -37,6 +37,7 @@ void SendGuildSubCommands(Client *c); void SendShowInventorySubCommands(Client *c); void SendFixMobSubCommands(Client *c); void SendDataBucketsSubCommands(Client *c); +void SendParcelsSubCommands(Client *c); // Commands void command_acceptrules(Client *c, const Seperator *sep); @@ -135,6 +136,7 @@ void command_nukebuffs(Client *c, const Seperator *sep); void command_nukeitem(Client *c, const Seperator *sep); void command_object(Client *c, const Seperator *sep); void command_oocmute(Client *c, const Seperator *sep); +void command_parcels(Client *c, const Seperator *sep); void command_path(Client *c, const Seperator *sep); void command_peqzone(Client *c, const Seperator *sep); void command_petitems(Client *c, const Seperator *sep); diff --git a/zone/gm_commands/parcels.cpp b/zone/gm_commands/parcels.cpp new file mode 100644 index 000000000..770b9ff00 --- /dev/null +++ b/zone/gm_commands/parcels.cpp @@ -0,0 +1,307 @@ +#include "../client.h" +#include "../worldserver.h" +#include "../../common/events/player_events.h" + +extern WorldServer worldserver; + +void command_parcels(Client *c, const Seperator *sep) +{ + const auto arguments = sep->argnum; + if (!arguments) { + SendParcelsSubCommands(c); + return; + } + + bool is_listdb = !strcasecmp(sep->arg[1], "listdb"); + bool is_listmemory = !strcasecmp(sep->arg[1], "listmemory"); + bool is_details = !strcasecmp(sep->arg[1], "details"); + bool is_add = !strcasecmp(sep->arg[1], "add"); + + if (!is_listdb && !is_listmemory && !is_details && !is_add) { + SendParcelsSubCommands(c); + return; + } + + if (is_listdb) { + auto player_name = std::string(sep->arg[2]); + if (arguments < 2) { + c->Message(Chat::White, "Usage: #parcels listdb [Character Name]"); + } + + if (player_name.empty()) { + c->Message( + Chat::White, + fmt::format("You must provide a player name.").c_str()); + return; + } + + auto player_id = CharacterParcelsRepository::GetParcelCountAndCharacterName(database, player_name); + if (!player_id.at(0).char_id) { + c->MessageString(Chat::Yellow, CANT_FIND_PLAYER, player_name.c_str()); + return; + } + + auto results = CharacterParcelsRepository::GetWhere( + database, + fmt::format("char_id = '{}' ORDER BY slot_id ASC", player_id.at(0).char_id) + ); + + if (results.empty()) { + c->Message(Chat::White, fmt::format("No parcels could be found for {}", player_name).c_str()); + return; + } + + c->Message(Chat::Yellow, fmt::format("Found {} parcels for {}.", results.size(), player_name).c_str()); + + for (auto const &p: results) { + c->Message( + Chat::Yellow, + fmt::format( + "Slot [{:02}] has item id [{:10}] with quantity [{}].", + p.slot_id, + p.item_id, + p.quantity + ).c_str() + ); + } + } + if (is_listmemory) { + auto player_name = std::string(sep->arg[2]); + auto player = entity_list.GetClientByName(player_name.c_str()); + + if (arguments < 2) { + c->Message(Chat::White, "Usage: #parcels listmemory [Character Name] (Must be in the same zone)"); + } + + if (!player) { + c->Message( + Chat::White, + fmt::format( + "Player {} could not be found in this zone. Ensure you are in the same zone as the player.", + player_name + ).c_str() + ); + return; + } + + auto parcels = player->GetParcels(); + if (parcels.empty()) { + c->Message(Chat::White, fmt::format("No parcels could be found for {}", player_name).c_str()); + return; + } + + c->Message(Chat::Yellow, fmt::format("Found {} parcels for {}.", parcels.size(), player_name).c_str()); + for (auto const &p: parcels) { + c->Message( + Chat::Yellow, + fmt::format( + "Slot [{:02}] has item id [{:10}] with quantity [{}].", + p.second.slot_id, + p.second.item_id, + p.second.quantity + ).c_str() + ); + } + } + if (is_add) { + if (arguments < 4) { + SendParcelsSubCommands(c); + return; + } + + if (!Strings::IsNumber(sep->arg[3]) || !Strings::IsNumber(sep->arg[4])) { + SendParcelsSubCommands(c); + return; + } + + auto to_name = std::string(sep->arg[2]); + auto item_id = Strings::ToUnsignedInt(sep->arg[3]); + auto quantity = Strings::ToUnsignedInt(sep->arg[4]); + auto note = std::string(sep->argplus[5]); + + auto send_to_client = CharacterParcelsRepository::GetParcelCountAndCharacterName( + database, + to_name + ); + if (send_to_client.at(0).character_name.empty()) { + c->MessageString(Chat::Yellow, CANT_FIND_PLAYER, to_name.c_str()); + return; + } + + auto next_slot = c->FindNextFreeParcelSlot(send_to_client.at(0).char_id); + if (next_slot == INVALID_INDEX) { + c->Message( + Chat::Yellow, + fmt::format( + "Unfortunately, {} cannot accept any more parcels at this time. Please try again later.", + send_to_client.at(0).character_name + ).c_str() + ); + return; + } + + if (item_id == PARCEL_MONEY_ITEM_ID) { + if (quantity > INT32_MAX) { + c->Message( + Chat::Yellow, + "Your quantity of {} copper pieces was too large. Set to max quantity of {}.", + quantity, + INT32_MAX + ); + quantity = INT32_MAX; + } + + auto item = database.GetItem(PARCEL_MONEY_ITEM_ID); + if (!item) { + c->Message(Chat::Yellow, "Could not find item with id {}", item_id); + return; + } + + std::unique_ptr inst(database.CreateItem(item, 1)); + if (!inst) { + c->Message(Chat::Yellow, "Could not find item with id {}", item_id); + return; + } + + CharacterParcelsRepository::CharacterParcels parcel_out; + parcel_out.from_name = c->GetName(); + parcel_out.note = note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity == 0 ? 1 : quantity; + parcel_out.item_id = PARCEL_MONEY_ITEM_ID; + parcel_out.char_id = send_to_client.at(0).char_id; + 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, + send_to_client.at(0).character_name, + parcel_out.item_id, + parcel_out.quantity + ); + c->Message( + Chat::Yellow, + "Unable to save parcel to the database. Please contact an administrator." + ); + return; + } + + c->MessageString( + Chat::Yellow, + PARCEL_DELIVERY, + c->GetCleanName(), + "Money", + send_to_client.at(0).character_name.c_str() + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + 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.quantity = parcel_out.quantity; + e.sent_date = parcel_out.sent_date; + + RecordPlayerEventLogWithClient(c, PlayerEvent::PARCEL_SEND, e); + } + + Parcel_Struct ps{}; + ps.item_slot = parcel_out.slot_id; + strn0cpy(ps.send_to, send_to_client.at(0).character_name.c_str(), sizeof(ps.send_to)); + + c->SendParcelDeliveryToWorld(ps); + } + else { + auto item = database.GetItem(item_id); + if (!item) { + c->Message(Chat::Yellow, "Could not find an item with id {}", item_id); + return; + } + + std::unique_ptr inst( + database.CreateItem( + item, + quantity > INT16_MAX ? INT16_MAX : (int16) quantity + ) + ); + if (!inst) { + c->Message(Chat::Yellow, "Could not find an item with id {}", item_id); + return; + } + + if (inst->IsStackable()) { + quantity = quantity > inst->GetItem()->StackSize + ? inst->GetItem()->StackSize : (int16) quantity; + } + else if (inst->GetItem()->MaxCharges > 0) { + quantity = quantity >= inst->GetItem()->MaxCharges + ? inst->GetItem()->MaxCharges : (int16) quantity; + } + + CharacterParcelsRepository::CharacterParcels parcel_out; + parcel_out.from_name = c->GetName(); + parcel_out.note = note.empty() ? "" : note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity; + parcel_out.item_id = item_id; + parcel_out.char_id = send_to_client.at(0).char_id; + 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, + send_to_client.at(0).character_name, + parcel_out.item_id, + parcel_out.quantity + ); + c->Message( + Chat::Yellow, + "Unable to save parcel to the database. Please contact an administrator." + ); + return; + } + + c->MessageString( + Chat::Yellow, + PARCEL_DELIVERY, + c->GetCleanName(), + inst->GetItem()->Name, + send_to_client.at(0).character_name.c_str() + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + 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.quantity = parcel_out.quantity; + e.sent_date = parcel_out.sent_date; + + RecordPlayerEventLogWithClient(c, PlayerEvent::PARCEL_SEND, e); + } + + Parcel_Struct ps{}; + ps.item_slot = parcel_out.slot_id; + strn0cpy(ps.send_to, send_to_client.at(0).character_name.c_str(), sizeof(ps.send_to)); + + c->SendParcelDeliveryToWorld(ps); + } + } +} + +void SendParcelsSubCommands(Client *c) +{ + c->Message(Chat::White, "#parcels listdb [Character Name]"); + c->Message(Chat::White, "#parcels listmemory [Character Name] (Must be in the same zone)"); + c->Message( + Chat::White, + "#parcels add [Character Name] [item id] [quantity] [note]. To send money use item id of 99990. Quantity is valid for stackable items, charges on an item, or amount of copper." + ); + c->Message(Chat::White, "#parcels details [Character Name]"); +} diff --git a/zone/npc.h b/zone/npc.h index c9f074a1b..3280f462a 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -268,6 +268,7 @@ public: inline void MerchantOpenShop() { merchant_open = true; } inline void MerchantCloseShop() { merchant_open = false; } inline bool IsMerchantOpen() { return merchant_open; } + inline bool GetParcelMerchant() { return NPCTypedata->is_parcel_merchant; } void Depop(bool start_spawn_timer = false); void Stun(int duration); void UnStun(); diff --git a/zone/parcels.cpp b/zone/parcels.cpp new file mode 100644 index 000000000..dc92a9d92 --- /dev/null +++ b/zone/parcels.cpp @@ -0,0 +1,752 @@ +/* EQEMu: Everquest Server Emulator + Copyright (C) 2001-2002 EQEMu Development Team (http://eqemu.org) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#include "../common/global_define.h" +#include "../common/events/player_event_logs.h" +#include "../common/repositories/trader_repository.h" +#include "../common/repositories/character_parcels_repository.h" +#include "worldserver.h" +#include "string_ids.h" +#include "client.h" +#include "../common/ruletypes.h" + +extern WorldServer worldserver; + +void Client::SendBulkParcels() +{ + SetEngagedWithParcelMerchant(true); + LoadParcels(); + + if (m_parcels.empty()) { + return; + } + + ParcelMessaging_Struct pms{}; + pms.packet_type = ItemPacketParcel; + + std::stringstream ss; + cereal::BinaryOutputArchive ar(ss); + + for (auto &p: m_parcels) { + auto item = database.GetItem(p.second.item_id); + if (item) { + std::unique_ptr inst(database.CreateItem(item, p.second.quantity)); + if (inst) { + inst->SetCharges(p.second.quantity > 0 ? p.second.quantity : 1); + inst->SetMerchantCount(1); + inst->SetMerchantSlot(p.second.slot_id); + if (inst->IsStackable()) { + inst->SetCharges(p.second.quantity); + } + + if (item->ID == PARCEL_MONEY_ITEM_ID) { + inst->SetPrice(p.second.quantity); + inst->SetCharges(1); + } + + pms.player_name = p.second.from_name; + pms.sent_time = p.second.sent_date; + pms.note = p.second.note; + pms.serialized_item = inst->Serialize(p.second.slot_id); + pms.slot_id = p.second.slot_id; + ar(pms); + + uint32 packet_size = ss.str().length(); + std::unique_ptr out(new EQApplicationPacket(OP_ItemPacket, packet_size)); + if (out->size != packet_size) { + LogError( + "Attempted to send a parcel packet of mismatched size {} with a buffer size of {}.", + out->Size(), + packet_size + ); + return; + } + memcpy(out->pBuffer, ss.str().data(), out->size); + QueuePacket(out.get()); + + ss.str(""); + ss.clear(); + } + } + + } + if (m_parcels.size() >= RuleI(Parcel, ParcelMaxItems) + PARCEL_LIMIT) { + LogError( + "Found {} parcels for Character {}. List truncated to the ParcelMaxItems rule [{}] + PARCEL_LIMIT.", + m_parcels.size(), + GetCleanName(), + RuleI(Parcel, ParcelMaxItems) + ); + SendParcelStatus(); + return; + } +} + +void Client::SendParcel(const Parcel_Struct &parcel_in) +{ + auto results = CharacterParcelsRepository::GetWhere( + database, + fmt::format( + "`char_id` = '{}' AND `slot_id` = '{}' LIMIT 1", + CharacterID(), + parcel_in.item_slot + ) + ); + + if (results.empty()) { + return; + } + + ParcelMessaging_Struct pms{}; + pms.packet_type = ItemPacketParcel; + + std::stringstream ss; + cereal::BinaryOutputArchive ar(ss); + + CharacterParcelsRepository::CharacterParcels parcel{}; + parcel.from_name = results[0].from_name; + parcel.id = results[0].id; + parcel.note = results[0].note; + parcel.quantity = results[0].quantity; + parcel.sent_date = results[0].sent_date; + parcel.item_id = results[0].item_id; + parcel.slot_id = results[0].slot_id; + parcel.char_id = results[0].char_id; + + auto item = database.GetItem(parcel.item_id); + if (item) { + std::unique_ptr inst(database.CreateItem(item, parcel.quantity)); + if (inst) { + inst->SetCharges(parcel.quantity > 0 ? parcel.quantity : 1); + inst->SetMerchantCount(1); + inst->SetMerchantSlot(parcel.slot_id); + if (inst->IsStackable()) { + inst->SetCharges(parcel.quantity); + } + + if (item->ID == PARCEL_MONEY_ITEM_ID) { + inst->SetPrice(parcel.quantity); + inst->SetCharges(1); + } + + pms.player_name = parcel.from_name; + pms.sent_time = parcel.sent_date; + pms.note = parcel.note; + pms.serialized_item = inst->Serialize(parcel.slot_id); + pms.slot_id = parcel.slot_id; + ar(pms); + + uint32 packet_size = ss.str().length(); + std::unique_ptr out(new EQApplicationPacket(OP_ItemPacket, packet_size)); + if (out->size != packet_size) { + LogError( + "Attempted to send a parcel packet of mismatched size {} with a buffer size of {}.", + out->Size(), + packet_size + ); + return; + } + + memcpy(out->pBuffer, ss.str().data(), out->size); + QueuePacket(out.get()); + + ss.str(""); + ss.clear(); + + m_parcels.emplace(parcel.slot_id, parcel); + } + } +} + +void Client::DoParcelCancel() +{ + if ( + m_parcel_platinum || + m_parcel_gold || + m_parcel_silver || + m_parcel_copper + ) { + m_pp.platinum += m_parcel_platinum; + m_pp.gold += m_parcel_gold; + m_pp.silver += m_parcel_silver; + m_pp.copper += m_parcel_copper; + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + SaveCurrency(); + SendMoneyUpdate(); + } +} + +void Client::SendParcelStatus() +{ + LoadParcels(); + + int32 num_of_parcels = GetParcelCount(); + if (num_of_parcels > 0) { + int32 num_over_limit = (num_of_parcels - RuleI(Parcel, ParcelMaxItems)) < 0 ? 0 : (num_of_parcels - RuleI(Parcel, ParcelMaxItems)); + if (num_of_parcels == RuleI(Parcel, ParcelMaxItems)) { + Message( + Chat::Red, + fmt::format( + "You have reached the limit of {} parcels in your mailbox. You will not be able to send parcels until you retrieve at least 1 parcel. ", + RuleI(Parcel, ParcelMaxItems) + ).c_str() + ); + } + else if (num_over_limit == 1) { + MessageString( + Chat::Red, + PARCEL_STATUS_1, + std::to_string(num_of_parcels).c_str(), + std::to_string(RuleI(Parcel, ParcelMaxItems)).c_str() + ); + } + else if (num_over_limit > 1) { + MessageString( + Chat::Red, + PARCEL_STATUS_2, + std::to_string(num_of_parcels).c_str(), + std::to_string(num_over_limit).c_str(), + std::to_string(RuleI(Parcel, ParcelMaxItems)).c_str() + ); + } + else { + Message( + Chat::Yellow, + fmt::format( + "You have {} parcels in your mailbox. Please visit a parcel merchant soon.", + num_of_parcels + ).c_str() + ); + } + } + SendParcelIconStatus(); +} + + +void Client::DoParcelSend(const Parcel_Struct *parcel_in) +{ + auto send_to_client = CharacterParcelsRepository::GetParcelCountAndCharacterName(database, parcel_in->send_to); + auto merchant = entity_list.GetMob(parcel_in->npc_id); + if (!merchant) { + SendParcelAck(); + return; + } + + auto num_of_parcels = GetParcelCount(); + if (num_of_parcels >= RuleI(Parcel, ParcelMaxItems)) { + SendParcelIconStatus(); + Message( + Chat::Yellow, + fmt::format( + "{} tells you, 'Unfortunately, I cannot send your parcel as you are at your parcel limit of {}. Please retrieve a parcel and try again.", + merchant->GetCleanName(), + RuleI(Parcel, ParcelMaxItems) + ).c_str() + ); + DoParcelCancel(); + SendParcelAck(); + return; + } + + if (send_to_client.at(0).parcel_count >= RuleI(Parcel, ParcelMaxItems)) { + Message( + Chat::Yellow, + fmt::format( + "{} tells you, 'Unfortunately, {} cannot accept any more parcels at this time. Please try again later.", + merchant->GetCleanName(), + send_to_client.at(0).character_name == GetCleanName() ? "you" : send_to_client.at(0).character_name + ).c_str() + ); + SendParcelAck(); + DoParcelCancel(); + return; + } + + if (GetParcelTimer()->Check()) { + SetParcelEnabled(true); + } + + if (!GetParcelEnabled()) { + MessageString(Chat::Yellow, PARCEL_DELAY, merchant->GetCleanName()); + DoParcelCancel(); + SendParcelAck(); + return; + } + auto next_slot = INVALID_INDEX; + if (!send_to_client.at(0).character_name.empty()) { + next_slot = FindNextFreeParcelSlot(send_to_client.at(0).char_id); + if (next_slot == INVALID_INDEX) { + Message( + Chat::Yellow, + fmt::format( + "{} tells you, 'Unfortunately, {} cannot accept any more parcels at this time. Please try again later.", + merchant->GetCleanName(), + send_to_client.at(0).character_name + ).c_str() + ); + SendParcelAck(); + DoParcelCancel(); + return; + } + } + + switch (parcel_in->money_flag) { + case PARCEL_SEND_ITEMS: { + auto inst = GetInv().GetItem(parcel_in->item_slot); + if (!inst) { + LogError( + "Handle_OP_ShopSendParcel Could not find item in inventory slot {} for character {}.", + parcel_in->item_slot, + GetCleanName() + ); + SendParcelAck(); + DoParcelCancel(); + return; + } + + if (send_to_client.at(0).character_name.empty()) { + MessageString( + Chat::Yellow, + PARCEL_UNKNOWN_NAME, + merchant->GetCleanName(), + parcel_in->send_to, + inst->GetItem()->Name + ); + SendParcelAck(); + DoParcelCancel(); + return; + } + + uint32 quantity{}; + if (inst->IsStackable()) { + quantity = parcel_in->quantity; + } + else { + quantity = inst->GetCharges() > 0 ? inst->GetCharges() : parcel_in->quantity; + } + + CharacterParcelsRepository::CharacterParcels parcel_out; + parcel_out.from_name = GetName(); + parcel_out.note = parcel_in->note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = quantity; + parcel_out.item_id = inst->GetID(); + parcel_out.char_id = send_to_client.at(0).char_id; + 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, + parcel_out.char_id, + parcel_out.item_id, + parcel_out.quantity + ); + Message(Chat::Yellow, "Unable to save parcel to the database. Please see an administrator."); + return; + } + + RemoveItem(parcel_out.item_id, parcel_out.quantity); + std::unique_ptr outapp(new EQApplicationPacket(OP_ShopSendParcel)); + QueuePacket(outapp.get()); + + if (inst->IsStackable() && (quantity - parcel_in->quantity > 0)) { + inst->SetCharges(quantity - parcel_in->quantity); + PutItemInInventory(parcel_in->item_slot, *inst, true); + } + + MessageString( + Chat::Yellow, + PARCEL_DELIVERY, + merchant->GetCleanName(), + inst->GetItem()->Name, + send_to_client.at(0).character_name.c_str() + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + 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.quantity = parcel_out.quantity; + 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, send_to_client.at(0).character_name.c_str(), sizeof(ps.send_to)); + + SendParcelDeliveryToWorld(ps); + + break; + } + case PARCEL_SEND_MONEY: { + auto item = database.GetItem(PARCEL_MONEY_ITEM_ID); + if (!item) { + DoParcelCancel(); + SendParcelAck(); + return; + } + + std::unique_ptr inst(database.CreateItem(item, 1)); + if (!inst) { + DoParcelCancel(); + SendParcelAck(); + return; + } + + if (send_to_client.at(0).character_name.empty()) { + MessageString( + Chat::Yellow, + PARCEL_UNKNOWN_NAME, + merchant->GetCleanName(), + parcel_in->send_to, + "Money" + ); + DoParcelCancel(); + SendParcelAck(); + return; + } + + CharacterParcelsRepository::CharacterParcels parcel_out; + parcel_out.from_name = GetName(); + parcel_out.note = parcel_in->note; + parcel_out.sent_date = time(nullptr); + parcel_out.quantity = parcel_in->quantity; + parcel_out.item_id = PARCEL_MONEY_ITEM_ID; + parcel_out.char_id = send_to_client.at(0).char_id; + 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, + send_to_client.at(0).character_name, + parcel_out.item_id, + parcel_out.quantity + ); + Message( + Chat::Yellow, + "Unable to save parcel to the database. Please see an administrator." + ); + return; + } + + MessageString( + Chat::Yellow, + PARCEL_DELIVERY, + merchant->GetCleanName(), + "Money", + send_to_client.at(0).character_name.c_str() + ); + + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) { + PlayerEvent::ParcelSend e{}; + 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.quantity = parcel_out.quantity; + e.sent_date = parcel_out.sent_date; + + RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e); + } + + m_parcel_platinum = 0; + m_parcel_gold = 0; + m_parcel_silver = 0; + m_parcel_copper = 0; + std::unique_ptr outapp(new EQApplicationPacket(OP_FinishTrade)); + QueuePacket(outapp.get()); + + Parcel_Struct ps{}; + ps.item_slot = parcel_out.slot_id; + strn0cpy(ps.send_to, send_to_client.at(0).character_name.c_str(), sizeof(ps.send_to)); + + SendParcelDeliveryToWorld(ps); + + break; + } + } + + SendParcelAck(); + SendParcelIconStatus(); + SetParcelEnabled(false); + GetParcelTimer()->Enable(); +} + +void Client::SendParcelAck() +{ + std::unique_ptr outapp(new EQApplicationPacket(OP_FinishTrade)); + QueuePacket(outapp.get()); + + std::unique_ptr outapp2(new EQApplicationPacket(OP_ShopSendParcel, sizeof(Parcel_Struct))); + auto data = (Parcel_Struct *) outapp2->pBuffer; + data->item_slot = 0xffffffff; + data->quantity = 0xffffffff; + QueuePacket(outapp2.get()); +} + +void Client::SendParcelRetrieveAck() +{ + std::unique_ptr outapp(new EQApplicationPacket(OP_ShopRetrieveParcel)); + QueuePacket(outapp.get()); +} + +void Client::SendParcelDeliveryToWorld(const Parcel_Struct &parcel) +{ + std::unique_ptr out(new ServerPacket(ServerOP_ParcelDelivery, sizeof(Parcel_Struct))); + auto data = (Parcel_Struct *) out->pBuffer; + + data->item_slot = parcel.item_slot; + strn0cpy(data->send_to, parcel.send_to, sizeof(data->send_to)); + + worldserver.SendPacket(out.get()); +} + +void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in) +{ + auto merchant = entity_list.GetNPCByID(parcel_in.merchant_entity_id); + if (!merchant) { + SendParcelRetrieveAck(); + return; + } + + auto p = m_parcels.find(parcel_in.parcel_slot_id); + if (p != m_parcels.end()) { + uint32 item_id = parcel_in.parcel_item_id; + uint32 item_quantity = p->second.quantity; + if (!item_id || !item_quantity) { + LogError( + "Attempt to retrieve parcel with erroneous item id or quantity for client character id {}.", + CharacterID() + ); + SendParcelRetrieveAck(); + return; + } + + std::unique_ptr inst(database.CreateItem(item_id, item_quantity)); + if (!inst) { + SendParcelRetrieveAck(); + return; + } + + switch (parcel_in.parcel_item_id) { + case PARCEL_MONEY_ITEM_ID: { + AddMoneyToPP(p->second.quantity, true); + MessageString( + Chat::Yellow, + PARCEL_DELIVERED, + merchant->GetCleanName(), + "Money", //inst->DetermineMoneyStringForParcels(p->second.quantity).c_str(), + p->second.from_name.c_str() + ); + break; + } + default: { + auto free_id = GetInv().FindFreeSlot(false, false); + if (CheckLoreConflict(inst->GetItem())) { + if (RuleB(Parcel, DeleteOnDuplicate)) { + MessageString(Chat::Yellow, PARCEL_DUPLICATE_DELETE, inst->GetItem()->Name); + } + else { + MessageString(Chat::Yellow, DUP_LORE); + SendParcelRetrieveAck(); + return; + } + } + else if (inst->IsStackable()) { + inst->SetCharges(item_quantity); + if (TryStacking(inst.get(), ItemPacketTrade, true, false)) { + MessageString( + Chat::Yellow, + PARCEL_DELIVERED_2, + merchant->GetCleanName(), + std::to_string(item_quantity).c_str(), + inst->GetItem()->Name, + p->second.from_name.c_str() + ); + } + else if (free_id != INVALID_INDEX) { + inst->SetCharges(item_quantity); + if (PutItemInInventory(free_id, *inst, true)) { + MessageString( + Chat::Yellow, + PARCEL_DELIVERED_2, + merchant->GetCleanName(), + std::to_string(item_quantity).c_str(), + inst->GetItem()->Name, + p->second.from_name.c_str() + ); + } + } + else { + MessageString(Chat::Yellow, PARCEL_INV_FULL, merchant->GetCleanName()); + SendParcelRetrieveAck(); + return; + } + } + else if (free_id != INVALID_INDEX) { + inst->SetCharges(item_quantity > 0 ? item_quantity : 1); + if (PutItemInInventory(free_id, *inst.get(), true)) { + MessageString( + Chat::Yellow, + PARCEL_DELIVERED, + merchant->GetCleanName(), + inst->GetItem()->Name, + p->second.from_name.c_str() + ); + } + else { + MessageString(Chat::Yellow, PARCEL_INV_FULL, merchant->GetCleanName()); + SendParcelRetrieveAck(); + return; + } + } + else { + MessageString(Chat::Yellow, PARCEL_INV_FULL, merchant->GetCleanName()); + SendParcelRetrieveAck(); + return; + } + } + } + + if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_RETRIEVE)) { + PlayerEvent::ParcelRetrieve e{}; + e.from_player_name = p->second.from_name; + e.item_id = p->second.item_id; + e.quantity = p->second.quantity; + e.sent_date = p->second.sent_date; + + RecordPlayerEventLog(PlayerEvent::PARCEL_RETRIEVE, e); + } + + DeleteParcel(p->second.id); + SendParcelDelete(parcel_in); + m_parcels.erase(p); + } + SendParcelRetrieveAck(); + SendParcelIconStatus(); +} + +bool Client::DeleteParcel(uint32 parcel_id) +{ + auto result = CharacterParcelsRepository::DeleteOne(database, parcel_id); + if (!result) { + LogError("Error deleting parcel id {} from the database.", parcel_id); + return false; + } + + auto it = std::find_if(m_parcels.cbegin(), m_parcels.cend(), [&](const auto &x) { return x.second.id == parcel_id; }); + SetParcelCount(GetParcelCount() - 1); + + return true; +} + +void Client::LoadParcels() +{ + m_parcels.clear(); + auto results = CharacterParcelsRepository::GetWhere(database, fmt::format("char_id = '{}'", CharacterID())); + + for (auto const &p: results) { + m_parcels.emplace(p.slot_id, p); + } + + SetParcelCount(m_parcels.size()); +} + +void Client::SendParcelDelete(const ParcelRetrieve_Struct &parcel_in) +{ + std::unique_ptr outapp(new EQApplicationPacket(OP_ShopDeleteParcel, sizeof(ParcelRetrieve_Struct))); + auto data = (ParcelRetrieve_Struct *) outapp->pBuffer; + + data->merchant_entity_id = parcel_in.merchant_entity_id; + data->player_entity_id = parcel_in.player_entity_id; + data->parcel_slot_id = parcel_in.parcel_slot_id; + data->parcel_item_id = parcel_in.parcel_item_id; + + QueuePacket(outapp.get()); +} + + +int32 Client::FindNextFreeParcelSlot(uint32 char_id) +{ + auto results = CharacterParcelsRepository::GetWhere( + database, + fmt::format("char_id = '{}' ORDER BY slot_id ASC", char_id) + ); + + if (results.empty()) { + return PARCEL_BEGIN_SLOT; + } + + for (uint32 i = PARCEL_BEGIN_SLOT; i <= RuleI(Parcel, ParcelMaxItems); i++) { + auto it = std::find_if(results.cbegin(), results.cend(), [&](const auto &x) { return x.slot_id == i; }); + if (it == results.end()) { + return i; + } + } + + return INVALID_INDEX; +} + +void Client::SendParcelIconStatus() +{ + std::unique_ptr outapp(new EQApplicationPacket(OP_ShopParcelIcon, sizeof(ParcelIcon_Struct))); + auto data = (ParcelIcon_Struct *) outapp->pBuffer; + + auto const num_of_parcels = GetParcelCount(); + + data->status = IconOn; + if (num_of_parcels == 0) { + data->status = IconOff; + } + else if (num_of_parcels > RuleI(Parcel, ParcelMaxItems)) { + data->status = Overlimit; + } + + QueuePacket(outapp.get()); +} + +void Client::AddParcel(CharacterParcelsRepository::CharacterParcels &parcel) +{ + auto result = CharacterParcelsRepository::InsertOne(database, parcel); + if (!result.id) { + LogError( + "Failed to add parcel to database. From {} to id {} item {} quantity {}", + parcel.from_name, + parcel.char_id, + parcel.item_id, + parcel.quantity + ); + Message( + Chat::Yellow, + "Unable to send parcel at this time. Please try again later." + ); + SendParcelAck(); + return; + } +} diff --git a/zone/string_ids.h b/zone/string_ids.h index 0fc8b2859..9b64ecbc1 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -192,6 +192,10 @@ #define PET_SPELLHOLD_SET_ON 702 //The pet spellhold mode has been set to on. #define PET_SPELLHOLD_SET_OFF 703 //The pet spellhold mode has been set to off. #define GUILD_NAME_IN_USE 711 //You cannot create a guild with that name, that guild already exists on this server. +#define PARCEL_DELAY 734 //%1 tells you, 'You must give me a chance to send the last parcel before I can send another!' +#define PARCEL_DUPLICATE_DELETE 737 //Duplicate lore items are not allowed! Your duplicate %1 has been deleted! +#define PARCEL_DELIVER_3 741 //%1 told you, 'I will deliver the stack of %2 %3 to %4 as soon as possible!' +#define PARCEL_INV_FULL 790 //%1 tells you, 'Your inventory appears full! Unable to retrieve parceled item.' #define AA_CAP 1000 //You have reached the AA point cap, and cannot gain any further experience until some of your stored AA point pool is used. #define GM_GAINXP 1002 //[GM] You have gained %1 AXP and %2 EXP (%3). #define MALE_SLAYUNDEAD 1007 //%1's holy blade cleanses his target!(%2) @@ -277,6 +281,7 @@ #define SPARKLES 1236 //Your %1 sparkles. #define GROWS_DIM 1237 //Your %1 grows dim. #define BEGINS_TO_SHINE 1238 //Your %1 begins to shine. +#define CANT_FIND_PLAYER 1276 //I can't find a player named %1! #define SURNAME_REJECTED 1374 //Your new surname was rejected. Please try a different name. #define GUILD_DISBANDED 1377 //Your guild has been disbanded! You are no longer a member of any guild. #define DUEL_DECLINE 1383 //%1 has declined your challenge to duel to the death. @@ -302,6 +307,7 @@ #define SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction. #define QUEUED_TELL 2458 //[queued] #define QUEUE_TELL_FULL 2459 //[zoing and queue is full] +#define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading. #define SUSPEND_MINION_UNSUSPEND 3267 //%1 tells you, 'I live again...' #define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.' #define ONLY_SUMMONED_PETS 3269 //3269 This effect only works with summoned pets. @@ -384,6 +390,8 @@ #define ALREADY_IN_GRP_RAID 5088 //% 1 rejects your invite because they are in a raid and you are not in theirs, or they are a raid group leader #define DUNGEON_SEALED 5141 //The gateway to the dungeon is sealed off to you. Perhaps you would be able to enter if you needed to adventure there. #define ADVENTURE_COMPLETE 5147 //You received %1 points for successfully completing the adventure. +#define PARCEL_STATUS_2 5433 //You currently have % 1 parcels in your mail and are % 2 parcels over the limit of % 3!If you do not retrieve at least % 2 parcels before you logout, they will be lost! +#define PARCEL_STATUS_1 5434 //You currently have % 1 parcels in your mail and are 1 parcel over the limit of % 2!If you do not retrieve at least 1 parcel before you logout, it will be lost! #define SUCCOR_FAIL 5169 //The portal collapes before you can escape! #define NO_PROPER_ACCESS 5410 //You don't have the proper access rights. #define AUGMENT_RESTRICTED 5480 //The item does not satisfy the augment's restrictions. @@ -411,6 +419,11 @@ #define TRANSFORM_COMPLETE 6327 //You have successfully transformed your %1. #define DETRANSFORM_FAILED 6341 //%1 has no transformation that can be removed. #define GUILD_PERMISSION_FAILED 6418 //You do not have permission to change access options. +#define PARCEL_DELIVERY_ARRIVED 6465 //You have received a new parcel delivery! +#define PARCEL_DELIVERY 6466 //%1 tells you, 'I will deliver the %2 to %3 as soon as possible!' +#define PARCEL_UNKNOWN_NAME 6467 //%1 tells you, 'Unfortunately, I don't know anyone by the name of %2. Here is your %3 back.'' +#define PARCEL_DELIVERED 6471 //%1 hands you the %2 that was sent from %3. +#define PARCEL_DELIVERED_2 6472 //%1 hands you the stack of %2 %3 that was sent from %4. #define GENERIC_STRING 6688 //%1 (used to any basic message) #define SENTINEL_TRIG_YOU 6724 //You have triggered your sentinel. #define SENTINEL_TRIG_OTHER 6725 //%1 has triggered your sentinel. diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 4adcd80f4..fa1da5ac9 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -3859,6 +3859,51 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } break; } + case ServerOP_ParcelDelivery: { + auto in = (Parcel_Struct *) pack->pBuffer; + + if (strlen(in->send_to) == 0) { + LogError( + "ServerOP_ParcelDelivery pack received with incorrect character_name of {}.", + in->send_to + ); + return; + } + + for (auto const &c: entity_list.GetClientList()) { + if (strcasecmp(c.second->GetCleanName(), in->send_to) == 0) { + c.second->MessageString( + Chat::Yellow, + PARCEL_DELIVERY_ARRIVED + ); + c.second->SendParcelStatus(); + if (c.second->GetEngagedWithParcelMerchant()) { + c.second->SendParcel(*in); + } + return; + } + } + + break; + } + case ServerOP_ParcelPrune: { + for (auto const &c: entity_list.GetClientList()) { + if (c.second->GetEngagedWithParcelMerchant()) { + c.second->Message( + Chat::Red, + "Parcel data has been updated. Please re-open the Merchant Window." + ); + c.second->SetEngagedWithParcelMerchant(false); + c.second->DoParcelCancel(); + + auto out = new EQApplicationPacket(OP_ShopEndConfirm); + c.second->QueuePacket(out); + safe_delete(out); + return; + } + } + break; + } default: { LogInfo("Unknown ZS Opcode [{}] size [{}]", (int)pack->opcode, pack->size); break; diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index f603656ff..9c5bc58d8 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1792,6 +1792,7 @@ const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load t->min_dmg = n.mindmg; t->max_dmg = n.maxdmg; t->attack_count = n.attack_count; + t->is_parcel_merchant = n.is_parcel_merchant ? true : false; if (!n.special_abilities.empty()) { strn0cpy(t->special_abilities, n.special_abilities.c_str(), 512); diff --git a/zone/zonedump.h b/zone/zonedump.h index 2d72730d4..07d14e105 100644 --- a/zone/zonedump.h +++ b/zone/zonedump.h @@ -154,6 +154,7 @@ struct NPCType int exp_mod; int heroic_strikethrough; bool keeps_sold_items; + bool is_parcel_merchant; }; #pragma pack()