[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 <akkadius1@gmail.com>
This commit is contained in:
Mitch Freeman 2024-04-20 23:15:56 -03:00 committed by GitHub
parent 64fefaebe4
commit fcffc6b3d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 2505 additions and 230 deletions

View File

@ -31,11 +31,13 @@
#include "../../common/path_manager.h" #include "../../common/path_manager.h"
#include "../../common/repositories/skill_caps_repository.h" #include "../../common/repositories/skill_caps_repository.h"
#include "../../common/file.h" #include "../../common/file.h"
#include "../../common/events/player_event_logs.h"
EQEmuLogSys LogSys; EQEmuLogSys LogSys;
WorldContentService content_service; WorldContentService content_service;
ZoneStore zone_store; ZoneStore zone_store;
PathManager path; PathManager path;
PlayerEventLogs player_event_logs;
void ExportSpells(SharedDatabase *db); void ExportSpells(SharedDatabase *db);
void ExportSkillCaps(SharedDatabase *db); void ExportSkillCaps(SharedDatabase *db);

View File

@ -29,11 +29,13 @@
#include "../../common/path_manager.h" #include "../../common/path_manager.h"
#include "../../common/repositories/base_data_repository.h" #include "../../common/repositories/base_data_repository.h"
#include "../../common/file.h" #include "../../common/file.h"
#include "../../common/events/player_event_logs.h"
EQEmuLogSys LogSys; EQEmuLogSys LogSys;
WorldContentService content_service; WorldContentService content_service;
ZoneStore zone_store; ZoneStore zone_store;
PathManager path; PathManager path;
PlayerEventLogs player_event_logs;
void ImportSpells(SharedDatabase *db); void ImportSpells(SharedDatabase *db);
void ImportSkillCaps(SharedDatabase *db); void ImportSkillCaps(SharedDatabase *db);

View File

@ -177,6 +177,7 @@ SET(repositories
repositories/base/base_character_leadership_abilities_repository.h repositories/base/base_character_leadership_abilities_repository.h
repositories/base/base_character_material_repository.h repositories/base/base_character_material_repository.h
repositories/base/base_character_memmed_spells_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_peqzone_flags_repository.h
repositories/base/base_character_pet_buffs_repository.h repositories/base/base_character_pet_buffs_repository.h
repositories/base/base_character_pet_info_repository.h repositories/base/base_character_pet_info_repository.h
@ -357,6 +358,7 @@ SET(repositories
repositories/character_leadership_abilities_repository.h repositories/character_leadership_abilities_repository.h
repositories/character_material_repository.h repositories/character_material_repository.h
repositories/character_memmed_spells_repository.h repositories/character_memmed_spells_repository.h
repositories/character_parcels_repository.h
repositories/character_peqzone_flags_repository.h repositories/character_peqzone_flags_repository.h
repositories/character_pet_buffs_repository.h repositories/character_pet_buffs_repository.h
repositories/character_pet_info_repository.h repositories/character_pet_info_repository.h

View File

@ -35,6 +35,7 @@
#include "../common/repositories/character_data_repository.h" #include "../common/repositories/character_data_repository.h"
#include "../common/repositories/character_languages_repository.h" #include "../common/repositories/character_languages_repository.h"
#include "../common/repositories/character_leadership_abilities_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/character_skills_repository.h"
#include "../common/repositories/data_buckets_repository.h" #include "../common/repositories/data_buckets_repository.h"
#include "../common/repositories/group_id_repository.h" #include "../common/repositories/group_id_repository.h"
@ -49,6 +50,7 @@
#include "../common/repositories/raid_members_repository.h" #include "../common/repositories/raid_members_repository.h"
#include "../common/repositories/reports_repository.h" #include "../common/repositories/reports_repository.h"
#include "../common/repositories/variables_repository.h" #include "../common/repositories/variables_repository.h"
#include "../common/events/player_event_logs.h"
// Disgrace: for windows compile // Disgrace: for windows compile
#ifdef _WINDOWS #ifdef _WINDOWS
@ -2042,3 +2044,42 @@ void Database::Decode(std::string &in)
in.at(i) -= char('0'); 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 <yellow>[{}] parcels that were over <yellow>[{}] days old.",
results.size(),
RuleI(Parcel, ParcelPruneDelay)
);
}

View File

@ -267,6 +267,7 @@ public:
void SourceDatabaseTableFromUrl(const std::string& table_name, const std::string& url); void SourceDatabaseTableFromUrl(const std::string& table_name, const std::string& url);
void SourceSqlFromUrl(const std::string& url); void SourceSqlFromUrl(const std::string& url);
void PurgeCharacterParcels();
void Encode(std::string &in); void Encode(std::string &in);
void Decode(std::string &in); void Decode(std::string &in);

View File

@ -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; ALTER TABLE `lootdrop_entries` ADD `content_flags_disabled` varchar(100) NULL;
)", )",
.content_schema_update = true .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 // -- template; copy/paste this when you need to create a new entry
// ManifestEntry{ // ManifestEntry{

View File

@ -59,6 +59,7 @@ namespace DatabaseSchema {
{"character_leadership_abilities", "id"}, {"character_leadership_abilities", "id"},
{"character_material", "id"}, {"character_material", "id"},
{"character_memmed_spells", "id"}, {"character_memmed_spells", "id"},
{"character_parcels", "char_id"},
{"character_pet_buffs", "char_id"}, {"character_pet_buffs", "char_id"},
{"character_pet_info", "char_id"}, {"character_pet_info", "char_id"},
{"character_pet_inventory", "char_id"}, {"character_pet_inventory", "char_id"},
@ -128,6 +129,7 @@ namespace DatabaseSchema {
"character_leadership_abilities", "character_leadership_abilities",
"character_material", "character_material",
"character_memmed_spells", "character_memmed_spells",
"character_parcels",
"character_pet_buffs", "character_pet_buffs",
"character_pet_info", "character_pet_info",
"character_pet_inventory", "character_pet_inventory",

View File

@ -510,6 +510,11 @@ N(OP_ShopEndConfirm),
N(OP_ShopItem), N(OP_ShopItem),
N(OP_ShopPlayerBuy), N(OP_ShopPlayerBuy),
N(OP_ShopPlayerSell), N(OP_ShopPlayerSell),
N(OP_ShopSendParcel),
N(OP_ShopDeleteParcel),
N(OP_ShopRespondParcel),
N(OP_ShopRetrieveParcel),
N(OP_ShopParcelIcon),
N(OP_ShopRequest), N(OP_ShopRequest),
N(OP_SimpleMessage), N(OP_SimpleMessage),
N(OP_SkillUpdate), N(OP_SkillUpdate),

View File

@ -1131,4 +1131,10 @@ namespace LeadershipAbilitySlot {
constexpr uint16 HealthOfTargetsTarget = 14; 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*/ #endif /*COMMON_EQ_CONSTANTS_H*/

View File

@ -27,6 +27,8 @@
#include "../common/version.h" #include "../common/version.h"
#include "emu_constants.h" #include "emu_constants.h"
#include "textures.h" #include "textures.h"
#include "../cereal/include/cereal/archives/binary.hpp"
#include "../cereal/include/cereal/types/string.hpp"
static const uint32 BUFF_COUNT = 42; static const uint32 BUFF_COUNT = 42;
@ -1533,8 +1535,7 @@ struct ExpUpdate_Struct
** Packet Types: See ItemPacketType enum ** Packet Types: See ItemPacketType enum
** **
*/ */
enum ItemPacketType enum ItemPacketType {
{
ItemPacketViewLink = 0x00, ItemPacketViewLink = 0x00,
ItemPacketMerchant = 0x64, ItemPacketMerchant = 0x64,
ItemPacketTradeView = 0x65, ItemPacketTradeView = 0x65,
@ -1546,9 +1547,22 @@ enum ItemPacketType
ItemPacketTributeItem = 0x6C, ItemPacketTributeItem = 0x6C,
ItemPacketGuildTribute = 0x6D, ItemPacketGuildTribute = 0x6D,
ItemPacketCharmUpdate = 0x6E, // noted as incorrect ItemPacketCharmUpdate = 0x6E, // noted as incorrect
ItemPacketRecovery = 0x71,
ItemPacketParcel = 0x73,
ItemPacketInvalid = 0xFF ItemPacketInvalid = 0xFF
}; };
enum MerchantWindowTabDisplay {
None = 0x00,
SellBuy = 0x01,
Recover = 0x02,
SellBuyRecover = 0x03,
Parcel = 0x04,
SellBuyParcel = 0x05,
RecoverParcel = 0x06,
SellBuyRecoverParcel = 0x07
};
//enum ItemPacketType //enum ItemPacketType
//{ //{
// ItemPacketMerchant = /*100*/ 0x64, // Titanium+ // ItemPacketMerchant = /*100*/ 0x64, // Titanium+
@ -2057,12 +2071,75 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct
/*000*/ uint32 npcid; // Merchant NPC's entity id {
/*004*/ uint32 playerid; /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*008*/ uint32 command; //1=open, 0=cancel/close /*004*/ uint32 player_id;
/*012*/ float rate; //cost multiplier, dosent work anymore /*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<class Archive>
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: Unknowns:
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE

View File

@ -699,6 +699,9 @@ void PlayerEventLogs::SetSettingsDefaults()
m_settings[PlayerEvent::ITEM_CREATION].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_ITEM].event_enabled = 1;
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].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++) { for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) {
m_settings[i].retention_days = RETENTION_DAYS_DEFAULT; m_settings[i].retention_days = RETENTION_DAYS_DEFAULT;

View File

@ -58,6 +58,9 @@ namespace PlayerEvent {
ITEM_CREATION, ITEM_CREATION,
GUILD_TRIBUTE_DONATE_ITEM, GUILD_TRIBUTE_DONATE_ITEM,
GUILD_TRIBUTE_DONATE_PLAT, GUILD_TRIBUTE_DONATE_PLAT,
PARCEL_SEND,
PARCEL_RETRIEVE,
PARCEL_DELETE,
MAX // dont remove MAX // dont remove
}; };
@ -66,7 +69,7 @@ namespace PlayerEvent {
// If event is unimplemented just tag (Unimplemented) in the name // If event is unimplemented just tag (Unimplemented) in the name
// Events don't get saved to the database if unimplemented or deprecated // Events don't get saved to the database if unimplemented or deprecated
// Events tagged as deprecated will get automatically removed // Events tagged as deprecated will get automatically removed
static const char *EventName[PlayerEvent::MAX] = { static const char *EventName[EventType::MAX] = {
"None", "None",
"GM Command", "GM Command",
"Zoning", "Zoning",
@ -116,7 +119,10 @@ namespace PlayerEvent {
"Killed Raid NPC", "Killed Raid NPC",
"Item Creation", "Item Creation",
"Guild Tribute Donate Item", "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 // 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<class Archive>
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<class Archive>
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<class Archive>
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 #endif //EQEMU_PLAYER_EVENTS_H

View File

@ -3089,21 +3089,6 @@ namespace RoF
FINISH_ENCODE(); 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(OP_SkillUpdate)
{ {
ENCODE_LENGTH_EXACT(SkillUpdate_Struct); ENCODE_LENGTH_EXACT(SkillUpdate_Struct);
@ -5047,19 +5032,6 @@ namespace RoF
FINISH_DIRECT_DECODE(); 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) DECODE(OP_Trader)
{ {
uint32 psize = __packet->size; uint32 psize = __packet->size;

View File

@ -1579,30 +1579,75 @@ namespace RoF2
*p = nullptr; *p = nullptr;
//store away the emu struct //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; switch(old_item_pkt->PacketType)
EQ::InternalSerializedItem_Struct* int_struct = (EQ::InternalSerializedItem_Struct*)(&__emu_buffer[4]); {
case ItemPacketParcel: {
ParcelMessaging_Struct pms{};
EQ::Util::MemoryStreamReader ss(reinterpret_cast<char *>(in->pBuffer), in->size);
cereal::BinaryInputArchive ar(ss);
ar(pms);
uint32 player_name_length = pms.player_name.length();
uint32 note_length = pms.note.length();
auto *int_struct = (EQ::InternalSerializedItem_Struct *) pms.serialized_item.data();
EQ::OutBuffer ob;
EQ::OutBuffer::pos_type last_pos = ob.tellp();
ob.write(reinterpret_cast<const char *>(&pms.packet_type), 4);
SerializeItem(ob, (const EQ::ItemInstance *) int_struct->inst, pms.slot_id, 0, ItemPacketParcel);
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;
}
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 *) &note_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 ob;
EQ::OutBuffer::pos_type last_pos = ob.tellp(); EQ::OutBuffer::pos_type last_pos = ob.tellp();
ob.write((const char*)__emu_buffer, 4); ob.write((const char *)__emu_buffer, 4);
SerializeItem(ob, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType); SerializeItem(ob, (const EQ::ItemInstance *)int_struct->inst, int_struct->slot_id, 0,
old_item_pkt->PacketType);
if (ob.tellp() == last_pos) { if (ob.tellp() == last_pos) {
LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", int_struct->slot_id); LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]",
delete in; int_struct->slot_id);
safe_delete_array(__emu_buffer);
safe_delete(in);
return; return;
} }
in->size = ob.size(); in->size = ob.size();
in->pBuffer = ob.detach(); in->pBuffer = ob.detach();
delete[] __emu_buffer; safe_delete_array(__emu_buffer);
dest->FastQueuePacket(&in, ack_req); dest->FastQueuePacket(&in, ack_req);
} }
}
}
ENCODE(OP_ItemVerifyReply) ENCODE(OP_ItemVerifyReply)
{ {
@ -3163,21 +3208,6 @@ namespace RoF2
FINISH_ENCODE(); 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(OP_SkillUpdate)
{ {
ENCODE_LENGTH_EXACT(SkillUpdate_Struct); ENCODE_LENGTH_EXACT(SkillUpdate_Struct);
@ -5307,15 +5337,17 @@ namespace RoF2
FINISH_DIRECT_DECODE(); FINISH_DIRECT_DECODE();
} }
DECODE(OP_ShopRequest) DECODE(OP_ShopSendParcel)
{ {
DECODE_LENGTH_EXACT(structs::Merchant_Click_Struct); DECODE_LENGTH_EXACT(structs::Parcel_Struct);
SETUP_DIRECT_DECODE(Merchant_Click_Struct, structs::Merchant_Click_Struct); SETUP_DIRECT_DECODE(Parcel_Struct, structs::Parcel_Struct);
IN(npcid); IN(npc_id);
IN(playerid); IN(quantity);
IN(command); IN(money_flag);
IN(rate); 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(); FINISH_DIRECT_DECODE();
} }
@ -5534,11 +5566,24 @@ namespace RoF2
//sprintf(hdr.unknown000, "06e0002Y1W00"); //sprintf(hdr.unknown000, "06e0002Y1W00");
snprintf(hdr.unknown000, sizeof(hdr.unknown000), "%016d", item->ID); 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; hdr.unknown004 = 0;
structs::InventorySlot_Struct slot_id; structs::InventorySlot_Struct slot_id{};
switch (packet_type) { switch (packet_type) {
case ItemPacketLoot: case ItemPacketLoot:
slot_id = ServerToRoF2CorpseSlot(slot_id_in); slot_id = ServerToRoF2CorpseSlot(slot_id_in);
@ -5553,12 +5598,14 @@ namespace RoF2
hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex); hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex);
hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex); hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex);
hdr.price = inst->GetPrice(); hdr.price = inst->GetPrice();
hdr.merchant_slot = (inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1); hdr.merchant_slot = ((inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1));
hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0); hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0);
hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber()); hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber());
hdr.unknown028 = 0; hdr.parcel_item_id = packet_type == ItemPacketParcel ? inst->GetID() : 0;
hdr.last_cast_time = inst->GetRecastTimestamp(); hdr.last_cast_time = inst->GetRecastTimestamp();
hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254) ? 0xFFFFFFFF : inst->GetCharges())); hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254)
? 0xFFFFFFFF
: inst->GetCharges()));
hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0); hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0);
hdr.unknown044 = 0; hdr.unknown044 = 0;
hdr.unknown048 = 0; hdr.unknown048 = 0;
@ -5621,9 +5668,10 @@ namespace RoF2
ob.write((const char*)&hdrf, sizeof(RoF2::structs::ItemSerializationHeaderFinish)); 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(item->Name, strlen(item->Name));
ob.write("\0", 1); ob.write("\0", 1);
}
if (strlen(item->Lore) > 0) if (strlen(item->Lore) > 0)
ob.write(item->Lore, strlen(item->Lore)); ob.write(item->Lore, strlen(item->Lore));
@ -5785,7 +5833,8 @@ namespace RoF2
itbs.potion_belt_enabled = item->PotionBelt; itbs.potion_belt_enabled = item->PotionBelt;
itbs.potion_belt_slots = item->PotionBeltSlots; itbs.potion_belt_slots = item->PotionBeltSlots;
itbs.stacksize = (inst->IsStackable() ? item->StackSize : 0); itbs.stacksize =
item->ID == PARCEL_MONEY_ITEM_ID ? 0x7FFFFFFF : ((inst->IsStackable() ? item->StackSize : 0));
itbs.no_transfer = item->NoTransfer; itbs.no_transfer = item->NoTransfer;
itbs.expendablearrow = item->ExpendableArrow; itbs.expendablearrow = item->ExpendableArrow;

View File

@ -116,7 +116,6 @@ E(OP_SendZonepoints)
E(OP_SetGuildRank) E(OP_SetGuildRank)
E(OP_ShopPlayerBuy) E(OP_ShopPlayerBuy)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SkillUpdate) E(OP_SkillUpdate)
E(OP_SomeItemPacketMaybe) E(OP_SomeItemPacketMaybe)
E(OP_SpawnAppearance) E(OP_SpawnAppearance)
@ -200,7 +199,7 @@ D(OP_Save)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerBuy) D(OP_ShopPlayerBuy)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest) D(OP_ShopSendParcel)
D(OP_Trader) D(OP_Trader)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)

View File

@ -2247,15 +2247,17 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct
/*000*/ uint32 npcid; // Merchant NPC's entity id {
/*004*/ uint32 playerid; /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*008*/ uint32 command; // 1=open, 0=cancel/close /*004*/ uint32 player_id;
/*012*/ float rate; // cost multiplier, dosent work anymore /*008*/ uint32 command; // 1=open, 0=cancel/close
/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client /*012*/ float rate; // cost multiplier, dosent work anymore
/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
/*024*/ /*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client
/*024*/
}; };
/* /*
Unknowns: Unknowns:
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
@ -4572,24 +4574,24 @@ struct RoF2SlotStruct
struct ItemSerializationHeader struct ItemSerializationHeader
{ {
/*000*/ char unknown000[17]; // New for HoT. Looks like a string. /*000*/ char unknown000[17]; // New for HoT. Looks like a string.
/*017*/ uint32 stacksize; /*017*/ uint32 stacksize;
/*021*/ uint32 unknown004; /*021*/ uint32 unknown004;
/*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ? /*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ?
/*026*/ uint16 main_slot; /*026*/ uint16 main_slot;
/*028*/ uint16 sub_slot; /*028*/ uint16 sub_slot;
/*030*/ uint16 aug_slot; // 0xffff /*030*/ uint16 aug_slot; // 0xffff
/*032*/ uint32 price; /*032*/ uint32 price;
/*036*/ uint32 merchant_slot; //1 if not a merchant item /*036*/ uint32 merchant_slot; // 1 if not a merchant item
/*040*/ uint32 scaled_value; //0 /*040*/ uint32 scaled_value; // 0
/*044*/ uint32 instance_id; //unique instance id if not merchant item, else is merchant slot /*044*/ uint32 instance_id; // unique instance id if not merchant item, else is merchant slot
/*048*/ uint32 unknown028; //0 /*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 /*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) /*056*/ uint32 charges; // Total Charges an item has (-1 for unlimited)
/*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items) /*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items)
/*064*/ uint32 unknown044; // 0 /*064*/ uint32 unknown044; // 0
/*068*/ uint32 unknown048; // 0 /*068*/ uint32 unknown048; // 0
/*072*/ uint32 unknown052; // 0 /*072*/ uint32 unknown052; // 0
uint8 isEvolving; uint8 isEvolving;
}; };
@ -5261,6 +5263,18 @@ struct Checksum_Struct {
uint8_t data[2048]; 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*/ }; /*structs*/
}; /*RoF2*/ }; /*RoF2*/

View File

@ -101,7 +101,6 @@ E(OP_SendZonepoints)
E(OP_SetGuildRank) E(OP_SetGuildRank)
E(OP_ShopPlayerBuy) E(OP_ShopPlayerBuy)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SkillUpdate) E(OP_SkillUpdate)
E(OP_SomeItemPacketMaybe) E(OP_SomeItemPacketMaybe)
E(OP_SpawnAppearance) E(OP_SpawnAppearance)
@ -183,7 +182,6 @@ D(OP_Save)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerBuy) D(OP_ShopPlayerBuy)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest)
D(OP_Trader) D(OP_Trader)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)

View File

@ -2200,15 +2200,17 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct
/*000*/ uint32 npcid; // Merchant NPC's entity id {
/*004*/ uint32 playerid; /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*008*/ uint32 command; // 1=open, 0=cancel/close /*004*/ uint32 player_id;
/*012*/ float rate; // cost multiplier, dosent work anymore /*008*/ uint32 command; // 1=open, 0=cancel/close
/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client /*012*/ float rate; // cost multiplier, dosent work anymore
/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client /*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
/*024*/ /*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client
/*024*/
}; };
/* /*
Unknowns: Unknowns:
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE

View File

@ -2026,6 +2026,19 @@ namespace SoD
FINISH_ENCODE(); 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) ENCODE(OP_SomeItemPacketMaybe)
{ {
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow // 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(); 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(OP_TraderBuy)
{ {
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);

View File

@ -78,6 +78,7 @@ E(OP_SendCharInfo)
E(OP_SendZonepoints) E(OP_SendZonepoints)
E(OP_ShopPlayerBuy) E(OP_ShopPlayerBuy)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SomeItemPacketMaybe) E(OP_SomeItemPacketMaybe)
E(OP_SpawnDoor) E(OP_SpawnDoor)
E(OP_SpecialMesg) E(OP_SpecialMesg)
@ -141,6 +142,7 @@ D(OP_Save)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerBuy) D(OP_ShopPlayerBuy)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)
D(OP_TributeItem) D(OP_TributeItem)

View File

@ -1845,12 +1845,16 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct
/*000*/ uint32 npcid; // Merchant NPC's entity id {
/*004*/ uint32 playerid; /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*008*/ uint32 command; //1=open, 0=cancel/close /*004*/ uint32 player_id;
/*012*/ float rate; //cost multiplier, dosent work anymore /*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: Unknowns:
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE

View File

@ -1683,6 +1683,19 @@ namespace SoF
FINISH_ENCODE(); 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) ENCODE(OP_SomeItemPacketMaybe)
{ {
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow // 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(); 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(OP_TraderBuy)
{ {
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);

View File

@ -72,6 +72,7 @@ E(OP_SendAATable)
E(OP_SendCharInfo) E(OP_SendCharInfo)
E(OP_SendZonepoints) E(OP_SendZonepoints)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SomeItemPacketMaybe) E(OP_SomeItemPacketMaybe)
E(OP_SpawnDoor) E(OP_SpawnDoor)
E(OP_SpecialMesg) E(OP_SpecialMesg)
@ -128,6 +129,7 @@ D(OP_ReadBook)
D(OP_Save) D(OP_Save)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)
D(OP_TributeItem) D(OP_TributeItem)

View File

@ -1859,12 +1859,16 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct
/*000*/ uint32 npcid; // Merchant NPC's entity id {
/*004*/ uint32 playerid; /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*008*/ uint32 command; //1=open, 0=cancel/close /*004*/ uint32 player_id;
/*012*/ float rate; //cost multiplier, dosent work anymore /*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: Unknowns:
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE 0 is e7 from 01 to // MAYBE SLOT IN PURCHASE

View File

@ -1858,6 +1858,19 @@ namespace Titanium
FINISH_ENCODE(); 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) ENCODE(OP_SpecialMesg)
{ {
EQApplicationPacket *in = *p; EQApplicationPacket *in = *p;
@ -2875,6 +2888,21 @@ namespace Titanium
FINISH_DIRECT_DECODE(); 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(OP_TraderBuy)
{ {
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);

View File

@ -74,6 +74,7 @@ E(OP_SendCharInfo)
E(OP_SendAATable) E(OP_SendAATable)
E(OP_SetFace) E(OP_SetFace)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SpawnAppearance) E(OP_SpawnAppearance)
E(OP_SpecialMesg) E(OP_SpecialMesg)
E(OP_TaskDescription) E(OP_TaskDescription)
@ -120,6 +121,7 @@ D(OP_RaidInvite)
D(OP_ReadBook) D(OP_ReadBook)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)
D(OP_TributeItem) D(OP_TributeItem)

View File

@ -1669,9 +1669,9 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct {
/*000*/ uint32 npcid; // Merchant NPC's entity id /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*004*/ uint32 playerid; /*004*/ uint32 player_id;
/*008*/ uint32 command; //1=open, 0=cancel/close /*008*/ uint32 command; //1=open, 0=cancel/close
/*012*/ float rate; //cost multiplier, dosent work anymore /*012*/ float rate; //cost multiplier, dosent work anymore
}; };

View File

@ -2454,6 +2454,19 @@ namespace UF
FINISH_ENCODE(); 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) ENCODE(OP_SomeItemPacketMaybe)
{ {
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow // 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(); 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(OP_TraderBuy)
{ {
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct); DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);

View File

@ -89,6 +89,7 @@ E(OP_SendZonepoints)
E(OP_SetGuildRank) E(OP_SetGuildRank)
E(OP_ShopPlayerBuy) E(OP_ShopPlayerBuy)
E(OP_ShopPlayerSell) E(OP_ShopPlayerSell)
E(OP_ShopRequest)
E(OP_SomeItemPacketMaybe) E(OP_SomeItemPacketMaybe)
E(OP_SpawnAppearance) E(OP_SpawnAppearance)
E(OP_SpawnDoor) E(OP_SpawnDoor)
@ -158,6 +159,7 @@ D(OP_Save)
D(OP_SetServerFilter) D(OP_SetServerFilter)
D(OP_ShopPlayerBuy) D(OP_ShopPlayerBuy)
D(OP_ShopPlayerSell) D(OP_ShopPlayerSell)
D(OP_ShopRequest)
D(OP_TraderBuy) D(OP_TraderBuy)
D(OP_TradeSkillCombine) D(OP_TradeSkillCombine)
D(OP_TributeItem) D(OP_TributeItem)

View File

@ -1916,9 +1916,9 @@ struct TimeOfDay_Struct {
}; };
// Darvik: shopkeeper structs // Darvik: shopkeeper structs
struct Merchant_Click_Struct { struct MerchantClick_Struct {
/*000*/ uint32 npcid; // Merchant NPC's entity id /*000*/ uint32 npc_id; // Merchant NPC's entity id
/*004*/ uint32 playerid; /*004*/ uint32 player_id;
/*008*/ uint32 command; //1=open, 0=cancel/close /*008*/ uint32 command; //1=open, 0=cancel/close
/*012*/ float rate; //cost multiplier, dosent work anymore /*012*/ float rate; //cost multiplier, dosent work anymore
}; };

View File

@ -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 <ctime>
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<std::string> Columns()
{
return {
"id",
"char_id",
"item_id",
"slot_id",
"quantity",
"from_name",
"note",
"sent_date",
};
}
static std::vector<std::string> 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<CharacterParcels> &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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.char_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.item_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.slot_id = row[3] ? static_cast<uint32_t>(strtoul(row[3], nullptr, 10)) : 0;
e.quantity = row[4] ? static_cast<uint32_t>(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<std::string> 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<std::string> 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<CharacterParcels> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> v;
auto results = db.QueryDatabase(
fmt::format(
"{} VALUES {}",
BaseInsert(),
Strings::Implode(",", insert_chunks)
)
);
return (results.Success() ? results.RowsAffected() : 0);
}
static std::vector<CharacterParcels> All(Database& db)
{
std::vector<CharacterParcels> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.char_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.item_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.slot_id = row[3] ? static_cast<uint32_t>(strtoul(row[3], nullptr, 10)) : 0;
e.quantity = row[4] ? static_cast<uint32_t>(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<CharacterParcels> GetWhere(Database& db, const std::string &where_filter)
{
std::vector<CharacterParcels> 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<uint32_t>(strtoul(row[0], nullptr, 10)) : 0;
e.char_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.item_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.slot_id = row[3] ? static_cast<uint32_t>(strtoul(row[3], nullptr, 10)) : 0;
e.quantity = row[4] ? static_cast<uint32_t>(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<std::string> 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<CharacterParcels> &entries
)
{
std::vector<std::string> insert_chunks;
for (auto &e: entries) {
std::vector<std::string> 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<std::string> 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

View File

@ -146,6 +146,7 @@ public:
int32_t heroic_strikethrough; int32_t heroic_strikethrough;
int32_t faction_amount; int32_t faction_amount;
uint8_t keeps_sold_items; uint8_t keeps_sold_items;
uint8_t is_parcel_merchant;
}; };
static std::string PrimaryKey() static std::string PrimaryKey()
@ -283,6 +284,7 @@ public:
"heroic_strikethrough", "heroic_strikethrough",
"faction_amount", "faction_amount",
"keeps_sold_items", "keeps_sold_items",
"is_parcel_merchant",
}; };
} }
@ -416,6 +418,7 @@ public:
"heroic_strikethrough", "heroic_strikethrough",
"faction_amount", "faction_amount",
"keeps_sold_items", "keeps_sold_items",
"is_parcel_merchant",
}; };
} }
@ -583,6 +586,7 @@ public:
e.heroic_strikethrough = 0; e.heroic_strikethrough = 0;
e.faction_amount = 0; e.faction_amount = 0;
e.keeps_sold_items = 1; e.keeps_sold_items = 1;
e.is_parcel_merchant = 0;
return e; return e;
} }
@ -746,6 +750,7 @@ public:
e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0; e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0;
e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0; e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0;
e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1; e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1;
e.is_parcel_merchant = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 0;
return e; return e;
} }
@ -905,6 +910,7 @@ public:
v.push_back(columns[124] + " = " + std::to_string(e.heroic_strikethrough)); 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[125] + " = " + std::to_string(e.faction_amount));
v.push_back(columns[126] + " = " + std::to_string(e.keeps_sold_items)); 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( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@ -1053,6 +1059,7 @@ public:
v.push_back(std::to_string(e.heroic_strikethrough)); 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.faction_amount));
v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@ -1209,6 +1216,7 @@ public:
v.push_back(std::to_string(e.heroic_strikethrough)); 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.faction_amount));
v.push_back(std::to_string(e.keeps_sold_items)); 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) + ")"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
} }
@ -1369,6 +1377,7 @@ public:
e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0; e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0;
e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0; e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0;
e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1; e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1;
e.is_parcel_merchant = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 0;
all_entries.push_back(e); all_entries.push_back(e);
} }
@ -1520,6 +1529,7 @@ public:
e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0; e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 0;
e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0; e.faction_amount = row[125] ? static_cast<int32_t>(atoi(row[125])) : 0;
e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1; e.keeps_sold_items = row[126] ? static_cast<uint8_t>(strtoul(row[126], nullptr, 10)) : 1;
e.is_parcel_merchant = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 0;
all_entries.push_back(e); 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.heroic_strikethrough));
v.push_back(std::to_string(e.faction_amount)); 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.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@ -1870,6 +1881,7 @@ public:
v.push_back(std::to_string(e.heroic_strikethrough)); 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.faction_amount));
v.push_back(std::to_string(e.keeps_sold_items)); 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) + ")"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
} }

View File

@ -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<ParcelCountAndCharacterName> GetParcelCountAndCharacterName(Database &db, const std::string &character_name)
{
std::vector<ParcelCountAndCharacterName> 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

View File

@ -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_BOOL(Items, DisableSpellFocusEffects, false, "Enable this to disable Spell Focus Effects on Items")
RULE_CATEGORY_END() 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_CATEGORY
#undef RULE_INT #undef RULE_INT
#undef RULE_REAL #undef RULE_REAL

View File

@ -11,7 +11,7 @@ namespace ServerEvents {
static const std::string EVENT_TYPE_RELOAD_WORLD = "reload_world"; 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_RULE_CHANGE = "rule_change";
static const std::string EVENT_TYPE_CONTENT_FLAG_CHANGE = "content_flag_change"; static const std::string EVENT_TYPE_CONTENT_FLAG_CHANGE = "content_flag_change";
} } // namespace ServerEvents
class ServerEventScheduler { class ServerEventScheduler {
public: public:

View File

@ -113,6 +113,9 @@
#define ServerOP_GuildSendGuildList 0x007E #define ServerOP_GuildSendGuildList 0x007E
#define ServerOP_GuildMembersList 0x007F #define ServerOP_GuildMembersList 0x007F
#define ServerOP_ParcelDelivery 0x0090
#define ServerOP_ParcelPrune 0x0091
#define ServerOP_RaidAdd 0x0100 //in use #define ServerOP_RaidAdd 0x0100 //in use
#define ServerOP_RaidRemove 0x0101 //in use #define ServerOP_RaidRemove 0x0101 //in use
#define ServerOP_RaidDisband 0x0102 //in use #define ServerOP_RaidDisband 0x0102 //in use

View File

@ -42,7 +42,7 @@
* Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt * 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 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9043
#endif #endif

View File

@ -32,11 +32,13 @@
#include "../common/content/world_content_service.h" #include "../common/content/world_content_service.h"
#include "../common/zone_store.h" #include "../common/zone_store.h"
#include "../common/path_manager.h" #include "../common/path_manager.h"
#include "../common/events/player_event_logs.h"
EQEmuLogSys LogSys; EQEmuLogSys LogSys;
WorldContentService content_service; WorldContentService content_service;
ZoneStore zone_store; ZoneStore zone_store;
PathManager path; PathManager path;
PlayerEventLogs player_event_logs;
#ifdef _WINDOWS #ifdef _WINDOWS
#include <direct.h> #include <direct.h>

View File

@ -467,6 +467,10 @@ OP_ShopEnd=0x30a8
OP_ShopEndConfirm=0x3196 OP_ShopEndConfirm=0x3196
OP_ShopPlayerBuy=0x0ddd OP_ShopPlayerBuy=0x0ddd
OP_ShopDelItem=0x724f OP_ShopDelItem=0x724f
OP_ShopSendParcel=0x3a5d
OP_ShopDeleteParcel=0x47f1
OP_ShopRetrieveParcel=0x7013
OP_ShopParcelIcon=0x46f0
# tradeskill stuff: # tradeskill stuff:
OP_ClickObject=0x4aa1 OP_ClickObject=0x4aa1

View File

@ -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);

View File

@ -87,6 +87,7 @@
#include "../common/path_manager.h" #include "../common/path_manager.h"
#include "../common/events/player_event_logs.h" #include "../common/events/player_event_logs.h"
#include "../common/skill_caps.h" #include "../common/skill_caps.h"
#include "../common/repositories/character_parcels_repository.h"
SkillCaps skill_caps; SkillCaps skill_caps;
ZoneStore zone_store; ZoneStore zone_store;
@ -176,6 +177,9 @@ int main(int argc, char **argv)
PurgeInstanceTimer.Start(450000); PurgeInstanceTimer.Start(450000);
Timer EQTimeTimer(600000); Timer EQTimeTimer(600000);
EQTimeTimer.Start(600000); EQTimeTimer.Start(600000);
Timer parcel_prune_timer(86400000);
parcel_prune_timer.Start(86400000);
// global loads // global loads
LogInfo("Loading launcher list"); LogInfo("Loading launcher list");
@ -420,6 +424,20 @@ int main(int argc, char **argv)
client_list.Process(); client_list.Process();
guild_mgr.Process(); guild_mgr.Process();
if (parcel_prune_timer.Check()) {
if (RuleB(Parcel, EnableParcelMerchants) && RuleB(Parcel, EnablePruning)) {
LogTrading(
"Parcel Prune process running for parcels over <red>[{}] days",
RuleI(Parcel, ParcelPruneDelay)
);
auto out = std::make_unique<ServerPacket>(ServerOP_ParcelPrune);
zoneserver_list.SendPacketToBootedZones(out.get());
database.PurgeCharacterParcels();
}
}
if (player_event_process_timer.Check()) { if (player_event_process_timer.Check()) {
player_event_logs.Process(); player_event_logs.Process();
} }

View File

@ -1,6 +1,7 @@
#include "world_event_scheduler.h" #include "world_event_scheduler.h"
#include "../common/servertalk.h" #include "../common/servertalk.h"
#include <ctime> #include <ctime>
#include "../common/rulesys.h"
void WorldEventScheduler::Process(ZSList *zs_list) void WorldEventScheduler::Process(ZSList *zs_list)
{ {
@ -31,13 +32,10 @@ void WorldEventScheduler::Process(ZSList *zs_list)
); );
for (auto &e: m_events) { for (auto &e: m_events) {
// discard uninteresting events as its less work to calculate time on events we don't care about // discard uninteresting events as its less work to calculate time on events we don't care about
// different processes are interested in different events // different processes are interested in different events
if ( if (e.event_type != ServerEvents::EVENT_TYPE_BROADCAST &&
e.event_type != ServerEvents::EVENT_TYPE_BROADCAST && e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD) {
e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD
) {
continue; continue;
} }

View File

@ -1730,6 +1730,19 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
} }
break; 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: { default: {
LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size);
DumpPacket(pack->pBuffer, pack->size); DumpPacket(pack->pBuffer, pack->size);

View File

@ -101,6 +101,7 @@ SET(zone_sources
npc_scale_manager.cpp npc_scale_manager.cpp
object.cpp object.cpp
oriented_bounding_box.cpp oriented_bounding_box.cpp
parcels.cpp
pathfinder_interface.cpp pathfinder_interface.cpp
pathfinder_nav_mesh.cpp pathfinder_nav_mesh.cpp
pathfinder_null.cpp pathfinder_null.cpp

View File

@ -184,7 +184,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
mob_close_scan_timer(6000), mob_close_scan_timer(6000),
position_update_timer(10000), position_update_timer(10000),
consent_throttle_timer(2000), 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)) { for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) {
SetFilter(client_filter, FilterShow); SetFilter(client_filter, FilterShow);
@ -375,6 +376,15 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
bot_owner_options[booBuffCounter] = false; bot_owner_options[booBuffCounter] = false;
bot_owner_options[booMonkWuMessage] = 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); SetBotPulling(false);
SetBotPrecombat(false); SetBotPrecombat(false);
@ -383,6 +393,10 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
} }
Client::~Client() { Client::~Client() {
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) {
DoParcelCancel();
}
mMovementManager->RemoveClient(this); mMovementManager->RemoveClient(this);
DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID()); DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID());

View File

@ -68,6 +68,7 @@ namespace EQ
#include "cheat_manager.h" #include "cheat_manager.h"
#include "../common/events/player_events.h" #include "../common/events/player_events.h"
#include "../common/data_verification.h" #include "../common/data_verification.h"
#include "../common/repositories/character_parcels_repository.h"
#ifdef _WINDOWS #ifdef _WINDOWS
// since windows defines these within windef.h (which windows.h include) // 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 ReturnTraderReq(const EQApplicationPacket* app,int16 traderitemcharges, uint32 itemid = 0);
void TradeRequestFailed(const EQApplicationPacket* app); void TradeRequestFailed(const EQApplicationPacket* app);
void BuyTraderItem(TraderBuy_Struct* tbs,Client* trader,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<void*>* event_details = nullptr); void FinishTrade(
Mob *with,
bool finalizer = false,
void *event_entry = nullptr,
std::list<void *> *event_details = nullptr
);
void SendZonePoints(); 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<uint32, CharacterParcelsRepository::CharacterParcels> GetParcels() { return m_parcels; }
int32 FindNextFreeParcelSlot(uint32 char_id);
void SendParcelIconStatus();
void SendBuyerResults(char *SearchQuery, uint32 SearchID); void SendBuyerResults(char *SearchQuery, uint32 SearchID);
void ShowBuyLines(const EQApplicationPacket *app); void ShowBuyLines(const EQApplicationPacket *app);
@ -1857,6 +1886,14 @@ private:
bool Trader; bool Trader;
bool Buyer; bool Buyer;
std::string BuyerWelcomeMessage; 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<uint32, CharacterParcelsRepository::CharacterParcels> m_parcels{};
int Haste; //precalced value int Haste; //precalced value
uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004 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 dynamiczone_removal_timer;
Timer task_request_timer; Timer task_request_timer;
Timer pick_lock_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::vec3 m_Proximity;
glm::vec4 last_position_before_bulk_update; glm::vec4 last_position_before_bulk_update;

View File

@ -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/events/player_event_logs.h"
#include "../common/repositories/character_stats_record_repository.h" #include "../common/repositories/character_stats_record_repository.h"
#include "dialogue_window.h" #include "dialogue_window.h"
#include "../common/rulesys.h"
extern QueryServ* QServ; extern QueryServ* QServ;
extern Zone* zone; extern Zone* zone;
@ -321,6 +322,8 @@ void MapOpcodes()
ConnectedOpcodes[OP_OpenGuildTributeMaster] = &Client::Handle_OP_OpenGuildTributeMaster; ConnectedOpcodes[OP_OpenGuildTributeMaster] = &Client::Handle_OP_OpenGuildTributeMaster;
ConnectedOpcodes[OP_OpenInventory] = &Client::Handle_OP_OpenInventory; ConnectedOpcodes[OP_OpenInventory] = &Client::Handle_OP_OpenInventory;
ConnectedOpcodes[OP_OpenTributeMaster] = &Client::Handle_OP_OpenTributeMaster; 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_PDeletePetition] = &Client::Handle_OP_PDeletePetition;
ConnectedOpcodes[OP_PetCommands] = &Client::Handle_OP_PetCommands; ConnectedOpcodes[OP_PetCommands] = &Client::Handle_OP_PetCommands;
ConnectedOpcodes[OP_Petition] = &Client::Handle_OP_Petition; 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()) { if (zone && zone->GetInstanceTimer()) {
bool is_permanent = false; bool is_permanent = false;
uint32 remaining_time = database.GetTimeRemainingInstance(zone->GetInstanceID(), is_permanent); 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) 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()) if (IsLFP())
worldserver.StopLFP(CharacterID()); worldserver.StopLFP(CharacterID());
@ -4324,6 +4337,12 @@ void Client::Handle_OP_CancelTrade(const EQApplicationPacket *app)
FinishTrade(this); FinishTrade(this);
trade->Reset(); trade->Reset();
} }
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) {
DoParcelCancel();
SetEngagedWithParcelMerchant(false);
}
EQApplicationPacket end_trade1(OP_FinishWindow, 0); EQApplicationPacket end_trade1(OP_FinishWindow, 0);
QueuePacket(&end_trade1); QueuePacket(&end_trade1);
@ -13877,6 +13896,11 @@ void Client::Handle_OP_Shielding(const EQApplicationPacket *app)
void Client::Handle_OP_ShopEnd(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); EQApplicationPacket empty(OP_ShopEndConfirm);
QueuePacket(&empty); QueuePacket(&empty);
return; return;
@ -14423,82 +14447,107 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app)
void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app) void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app)
{ {
if (app->size != sizeof(Merchant_Click_Struct)) { if (app->size != sizeof(MerchantClick_Struct)) {
LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(Merchant_Click_Struct)); LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(MerchantClick_Struct));
return; 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. // Send back opcode OP_ShopRequest - tells client to open merchant window.
//EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); // EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct));
//Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer; // Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer;
int merchantid = 0; int merchant_id = 0;
Mob* tmp = entity_list.GetMob(mc->npcid); 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; return;
}
//you have to be somewhat close to them to be properly using them // you have to be somewhat close to them to be properly using them
if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) {
return; return;
}
merchantid = tmp->CastToNPC()->MerchantType; merchant_id = tmp->CastToNPC()->MerchantType;
int action = 1; if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && tmp->CastToNPC()->GetParcelMerchant()) {
if (merchantid == 0) { tabs_to_display = SellBuyParcel;
auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); }
Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer; else {
mco->npcid = mc->npcid; tabs_to_display = SellBuy;
mco->playerid = 0; }
mco->command = 1; //open...
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->rate = 1.0;
mco->tab_display = tabs_to_display;
QueuePacket(outapp); QueuePacket(outapp);
safe_delete(outapp); safe_delete(outapp);
return; return;
} }
if (tmp->IsEngaged()) { if (tmp->IsEngaged()) {
MessageString(Chat::White, MERCHANT_BUSY); MessageString(Chat::White, MERCHANT_BUSY);
action = 0; action = MerchantActions::Close;
}
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;
} }
if (tmp->Charmed()) if (GetFeigned() || IsInvisible()) {
action = 0; 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()) { if (!tmp->CastToNPC()->IsMerchantOpen()) {
tmp->SayString(zone->random.Int(MERCHANT_CLOSED_ONE, MERCHANT_CLOSED_THREE)); 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)); auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(MerchantClick_Struct));
Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer; auto mco = (MerchantClick_Struct *) outapp->pBuffer;
mco->npcid = mc->npcid; mco->npc_id = mc->npc_id;
mco->playerid = 0; mco->player_id = 0;
mco->command = action; // Merchant command 0x01 = open mco->command = action; // Merchant command 0x01 = open
mco->tab_display = tabs_to_display;
if (RuleB(Merchant, UsePriceMod)) { 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)); mco->rate = 1 / (RuleR(Merchant, BuyCostMod));
}
outapp->priority = 6; outapp->priority = 6;
QueuePacket(outapp); QueuePacket(outapp);
safe_delete(outapp); safe_delete(outapp);
if (action == 1) if (action == MerchantActions::Open) {
BulkSendMerchantInventory(merchantid, tmp->GetNPCTypeID()); BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID());
if ((tabs_to_display & Parcel) == Parcel) {
SendBulkParcels();
}
}
return; 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);
}

View File

@ -335,3 +335,6 @@
void Handle_OP_SharedTaskAccept(const EQApplicationPacket *app); void Handle_OP_SharedTaskAccept(const EQApplicationPacket *app);
void Handle_OP_SharedTaskQuit(const EQApplicationPacket *app); void Handle_OP_SharedTaskQuit(const EQApplicationPacket *app);
void Handle_OP_SharedTaskPlayerList(const EQApplicationPacket *app); void Handle_OP_SharedTaskPlayerList(const EQApplicationPacket *app);
void Handle_OP_ShopSendParcel(const EQApplicationPacket *app);
void Handle_OP_ShopRetrieveParcel(const EQApplicationPacket *app);

View File

@ -555,6 +555,7 @@ bool Client::Process() {
guild_mgr.UpdateDbMemberOnline(CharacterID(), false); guild_mgr.UpdateDbMemberOnline(CharacterID(), false);
guild_mgr.SendToWorldSendGuildMembersList(GuildID()); guild_mgr.SendToWorldSendGuildMembersList(GuildID());
} }
return false; return false;
} }
else if (!linkdead_timer.Enabled()) { else if (!linkdead_timer.Enabled()) {
@ -1429,6 +1430,22 @@ void Client::OPMoveCoin(const EQApplicationPacket* app)
to_bucket = (int32 *) &trade->cp; break; 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; break;
} }
case 4: // shared bank case 4: // shared bank

View File

@ -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("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("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("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("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("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) || 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/nukeitem.cpp"
#include "gm_commands/object.cpp" #include "gm_commands/object.cpp"
#include "gm_commands/object_manipulation.cpp" #include "gm_commands/object_manipulation.cpp"
#include "gm_commands/parcels.cpp"
#include "gm_commands/path.cpp" #include "gm_commands/path.cpp"
#include "gm_commands/peqzone.cpp" #include "gm_commands/peqzone.cpp"
#include "gm_commands/petitems.cpp" #include "gm_commands/petitems.cpp"

View File

@ -37,6 +37,7 @@ void SendGuildSubCommands(Client *c);
void SendShowInventorySubCommands(Client *c); void SendShowInventorySubCommands(Client *c);
void SendFixMobSubCommands(Client *c); void SendFixMobSubCommands(Client *c);
void SendDataBucketsSubCommands(Client *c); void SendDataBucketsSubCommands(Client *c);
void SendParcelsSubCommands(Client *c);
// Commands // Commands
void command_acceptrules(Client *c, const Seperator *sep); 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_nukeitem(Client *c, const Seperator *sep);
void command_object(Client *c, const Seperator *sep); void command_object(Client *c, const Seperator *sep);
void command_oocmute(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_path(Client *c, const Seperator *sep);
void command_peqzone(Client *c, const Seperator *sep); void command_peqzone(Client *c, const Seperator *sep);
void command_petitems(Client *c, const Seperator *sep); void command_petitems(Client *c, const Seperator *sep);

View File

@ -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<EQ::ItemInstance> 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<EQ::ItemInstance> 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]");
}

View File

@ -268,6 +268,7 @@ public:
inline void MerchantOpenShop() { merchant_open = true; } inline void MerchantOpenShop() { merchant_open = true; }
inline void MerchantCloseShop() { merchant_open = false; } inline void MerchantCloseShop() { merchant_open = false; }
inline bool IsMerchantOpen() { return merchant_open; } inline bool IsMerchantOpen() { return merchant_open; }
inline bool GetParcelMerchant() { return NPCTypedata->is_parcel_merchant; }
void Depop(bool start_spawn_timer = false); void Depop(bool start_spawn_timer = false);
void Stun(int duration); void Stun(int duration);
void UnStun(); void UnStun();

752
zone/parcels.cpp Normal file
View File

@ -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<EQ::ItemInstance> 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<EQApplicationPacket> 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<EQ::ItemInstance> 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<EQApplicationPacket> 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<EQApplicationPacket> 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<EQ::ItemInstance> 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<EQApplicationPacket> 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<EQApplicationPacket> outapp(new EQApplicationPacket(OP_FinishTrade));
QueuePacket(outapp.get());
std::unique_ptr<EQApplicationPacket> 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<EQApplicationPacket> outapp(new EQApplicationPacket(OP_ShopRetrieveParcel));
QueuePacket(outapp.get());
}
void Client::SendParcelDeliveryToWorld(const Parcel_Struct &parcel)
{
std::unique_ptr<ServerPacket> 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<EQ::ItemInstance> 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<EQApplicationPacket> 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<EQApplicationPacket> 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;
}
}

View File

@ -192,6 +192,10 @@
#define PET_SPELLHOLD_SET_ON 702 //The pet spellhold mode has been set to on. #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 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 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 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 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) #define MALE_SLAYUNDEAD 1007 //%1's holy blade cleanses his target!(%2)
@ -277,6 +281,7 @@
#define SPARKLES 1236 //Your %1 sparkles. #define SPARKLES 1236 //Your %1 sparkles.
#define GROWS_DIM 1237 //Your %1 grows dim. #define GROWS_DIM 1237 //Your %1 grows dim.
#define BEGINS_TO_SHINE 1238 //Your %1 begins to shine. #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 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 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. #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 SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction.
#define QUEUED_TELL 2458 //[queued] #define QUEUED_TELL 2458 //[queued]
#define QUEUE_TELL_FULL 2459 //[zoing and queue is full] #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_UNSUSPEND 3267 //%1 tells you, 'I live again...'
#define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.' #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. #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 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 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 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 SUCCOR_FAIL 5169 //The portal collapes before you can escape!
#define NO_PROPER_ACCESS 5410 //You don't have the proper access rights. #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. #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 TRANSFORM_COMPLETE 6327 //You have successfully transformed your %1.
#define DETRANSFORM_FAILED 6341 //%1 has no transformation that can be removed. #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 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 GENERIC_STRING 6688 //%1 (used to any basic message)
#define SENTINEL_TRIG_YOU 6724 //You have triggered your sentinel. #define SENTINEL_TRIG_YOU 6724 //You have triggered your sentinel.
#define SENTINEL_TRIG_OTHER 6725 //%1 has triggered your sentinel. #define SENTINEL_TRIG_OTHER 6725 //%1 has triggered your sentinel.

View File

@ -3859,6 +3859,51 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
} }
break; 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: { default: {
LogInfo("Unknown ZS Opcode [{}] size [{}]", (int)pack->opcode, pack->size); LogInfo("Unknown ZS Opcode [{}] size [{}]", (int)pack->opcode, pack->size);
break; break;

View File

@ -1792,6 +1792,7 @@ const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load
t->min_dmg = n.mindmg; t->min_dmg = n.mindmg;
t->max_dmg = n.maxdmg; t->max_dmg = n.maxdmg;
t->attack_count = n.attack_count; t->attack_count = n.attack_count;
t->is_parcel_merchant = n.is_parcel_merchant ? true : false;
if (!n.special_abilities.empty()) { if (!n.special_abilities.empty()) {
strn0cpy(t->special_abilities, n.special_abilities.c_str(), 512); strn0cpy(t->special_abilities, n.special_abilities.c_str(), 512);

View File

@ -154,6 +154,7 @@ struct NPCType
int exp_mod; int exp_mod;
int heroic_strikethrough; int heroic_strikethrough;
bool keeps_sold_items; bool keeps_sold_items;
bool is_parcel_merchant;
}; };
#pragma pack() #pragma pack()