mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 16:51:29 +00:00
[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:
parent
64fefaebe4
commit
fcffc6b3d4
@ -31,11 +31,13 @@
|
||||
#include "../../common/path_manager.h"
|
||||
#include "../../common/repositories/skill_caps_repository.h"
|
||||
#include "../../common/file.h"
|
||||
#include "../../common/events/player_event_logs.h"
|
||||
|
||||
EQEmuLogSys LogSys;
|
||||
EQEmuLogSys LogSys;
|
||||
WorldContentService content_service;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
PlayerEventLogs player_event_logs;
|
||||
|
||||
void ExportSpells(SharedDatabase *db);
|
||||
void ExportSkillCaps(SharedDatabase *db);
|
||||
|
||||
@ -29,11 +29,13 @@
|
||||
#include "../../common/path_manager.h"
|
||||
#include "../../common/repositories/base_data_repository.h"
|
||||
#include "../../common/file.h"
|
||||
#include "../../common/events/player_event_logs.h"
|
||||
|
||||
EQEmuLogSys LogSys;
|
||||
EQEmuLogSys LogSys;
|
||||
WorldContentService content_service;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
PlayerEventLogs player_event_logs;
|
||||
|
||||
void ImportSpells(SharedDatabase *db);
|
||||
void ImportSkillCaps(SharedDatabase *db);
|
||||
|
||||
@ -177,6 +177,7 @@ SET(repositories
|
||||
repositories/base/base_character_leadership_abilities_repository.h
|
||||
repositories/base/base_character_material_repository.h
|
||||
repositories/base/base_character_memmed_spells_repository.h
|
||||
repositories/base/base_character_parcels_repository.h
|
||||
repositories/base/base_character_peqzone_flags_repository.h
|
||||
repositories/base/base_character_pet_buffs_repository.h
|
||||
repositories/base/base_character_pet_info_repository.h
|
||||
@ -357,6 +358,7 @@ SET(repositories
|
||||
repositories/character_leadership_abilities_repository.h
|
||||
repositories/character_material_repository.h
|
||||
repositories/character_memmed_spells_repository.h
|
||||
repositories/character_parcels_repository.h
|
||||
repositories/character_peqzone_flags_repository.h
|
||||
repositories/character_pet_buffs_repository.h
|
||||
repositories/character_pet_info_repository.h
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
#include "../common/repositories/character_data_repository.h"
|
||||
#include "../common/repositories/character_languages_repository.h"
|
||||
#include "../common/repositories/character_leadership_abilities_repository.h"
|
||||
#include "../common/repositories/character_parcels_repository.h"
|
||||
#include "../common/repositories/character_skills_repository.h"
|
||||
#include "../common/repositories/data_buckets_repository.h"
|
||||
#include "../common/repositories/group_id_repository.h"
|
||||
@ -49,6 +50,7 @@
|
||||
#include "../common/repositories/raid_members_repository.h"
|
||||
#include "../common/repositories/reports_repository.h"
|
||||
#include "../common/repositories/variables_repository.h"
|
||||
#include "../common/events/player_event_logs.h"
|
||||
|
||||
// Disgrace: for windows compile
|
||||
#ifdef _WINDOWS
|
||||
@ -2042,3 +2044,42 @@ void Database::Decode(std::string &in)
|
||||
in.at(i) -= char('0');
|
||||
}
|
||||
};
|
||||
|
||||
void Database::PurgeCharacterParcels()
|
||||
{
|
||||
auto filter = fmt::format("sent_date < (NOW() - INTERVAL {} DAY)", RuleI(Parcel, ParcelPruneDelay));
|
||||
auto results = CharacterParcelsRepository::GetWhere(*this, filter);
|
||||
auto prune = CharacterParcelsRepository::DeleteWhere(*this, filter);
|
||||
|
||||
PlayerEvent::ParcelDelete pd{};
|
||||
PlayerEventLogsRepository::PlayerEventLogs pel{};
|
||||
pel.event_type_id = PlayerEvent::PARCEL_DELETE;
|
||||
pel.event_type_name = PlayerEvent::EventName[pel.event_type_id];
|
||||
std::stringstream ss;
|
||||
for (auto const &r: results) {
|
||||
pd.from_name = r.from_name;
|
||||
pd.item_id = r.item_id;
|
||||
pd.note = r.note;
|
||||
pd.quantity = r.quantity;
|
||||
pd.sent_date = r.sent_date;
|
||||
pd.char_id = r.char_id;
|
||||
{
|
||||
cereal::JSONOutputArchiveSingleLine ar(ss);
|
||||
pd.serialize(ar);
|
||||
}
|
||||
|
||||
pel.event_data = ss.str();
|
||||
pel.created_at = std::time(nullptr);
|
||||
|
||||
player_event_logs.AddToQueue(pel);
|
||||
|
||||
ss.str("");
|
||||
ss.clear();
|
||||
}
|
||||
|
||||
LogInfo(
|
||||
"Purged <yellow>[{}] parcels that were over <yellow>[{}] days old.",
|
||||
results.size(),
|
||||
RuleI(Parcel, ParcelPruneDelay)
|
||||
);
|
||||
}
|
||||
|
||||
@ -267,6 +267,7 @@ public:
|
||||
|
||||
void SourceDatabaseTableFromUrl(const std::string& table_name, const std::string& url);
|
||||
void SourceSqlFromUrl(const std::string& url);
|
||||
void PurgeCharacterParcels();
|
||||
void Encode(std::string &in);
|
||||
void Decode(std::string &in);
|
||||
|
||||
|
||||
@ -5494,6 +5494,32 @@ ALTER TABLE `lootdrop_entries` ADD `content_flags` varchar(100) NULL;
|
||||
ALTER TABLE `lootdrop_entries` ADD `content_flags_disabled` varchar(100) NULL;
|
||||
)",
|
||||
.content_schema_update = true
|
||||
},
|
||||
ManifestEntry{
|
||||
.version = 9271,
|
||||
.description = "2024_03_10_parcel_implementation.sql",
|
||||
.check = "SHOW TABLES LIKE 'character_parcels'",
|
||||
.condition = "empty",
|
||||
.match = "",
|
||||
.sql = R"(CREATE TABLE `character_parcels` (
|
||||
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`char_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
|
||||
`item_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
|
||||
`slot_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
|
||||
`quantity` INT(10) UNSIGNED NOT NULL DEFAULT '0',
|
||||
`from_name` VARCHAR(64) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
|
||||
`note` VARCHAR(1024) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
|
||||
`sent_date` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `data_constraint` (`slot_id`, `char_id`) USING BTREE
|
||||
)
|
||||
COLLATE='latin1_swedish_ci'
|
||||
ENGINE=InnoDB
|
||||
AUTO_INCREMENT=1;
|
||||
|
||||
ALTER TABLE `npc_types`
|
||||
ADD COLUMN `is_parcel_merchant` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `keeps_sold_items`;
|
||||
)"
|
||||
}
|
||||
// -- template; copy/paste this when you need to create a new entry
|
||||
// ManifestEntry{
|
||||
|
||||
@ -59,6 +59,7 @@ namespace DatabaseSchema {
|
||||
{"character_leadership_abilities", "id"},
|
||||
{"character_material", "id"},
|
||||
{"character_memmed_spells", "id"},
|
||||
{"character_parcels", "char_id"},
|
||||
{"character_pet_buffs", "char_id"},
|
||||
{"character_pet_info", "char_id"},
|
||||
{"character_pet_inventory", "char_id"},
|
||||
@ -128,6 +129,7 @@ namespace DatabaseSchema {
|
||||
"character_leadership_abilities",
|
||||
"character_material",
|
||||
"character_memmed_spells",
|
||||
"character_parcels",
|
||||
"character_pet_buffs",
|
||||
"character_pet_info",
|
||||
"character_pet_inventory",
|
||||
|
||||
@ -510,6 +510,11 @@ N(OP_ShopEndConfirm),
|
||||
N(OP_ShopItem),
|
||||
N(OP_ShopPlayerBuy),
|
||||
N(OP_ShopPlayerSell),
|
||||
N(OP_ShopSendParcel),
|
||||
N(OP_ShopDeleteParcel),
|
||||
N(OP_ShopRespondParcel),
|
||||
N(OP_ShopRetrieveParcel),
|
||||
N(OP_ShopParcelIcon),
|
||||
N(OP_ShopRequest),
|
||||
N(OP_SimpleMessage),
|
||||
N(OP_SkillUpdate),
|
||||
|
||||
@ -1131,4 +1131,10 @@ namespace LeadershipAbilitySlot {
|
||||
constexpr uint16 HealthOfTargetsTarget = 14;
|
||||
}
|
||||
|
||||
#define PARCEL_SEND_ITEMS 0
|
||||
#define PARCEL_SEND_MONEY 1
|
||||
#define PARCEL_MONEY_ITEM_ID 99990 // item id of money
|
||||
#define PARCEL_LIMIT 5
|
||||
#define PARCEL_BEGIN_SLOT 1
|
||||
|
||||
#endif /*COMMON_EQ_CONSTANTS_H*/
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
#include "../common/version.h"
|
||||
#include "emu_constants.h"
|
||||
#include "textures.h"
|
||||
#include "../cereal/include/cereal/archives/binary.hpp"
|
||||
#include "../cereal/include/cereal/types/string.hpp"
|
||||
|
||||
|
||||
static const uint32 BUFF_COUNT = 42;
|
||||
@ -1533,20 +1535,32 @@ struct ExpUpdate_Struct
|
||||
** Packet Types: See ItemPacketType enum
|
||||
**
|
||||
*/
|
||||
enum ItemPacketType
|
||||
{
|
||||
ItemPacketViewLink = 0x00,
|
||||
ItemPacketMerchant = 0x64,
|
||||
ItemPacketTradeView = 0x65,
|
||||
ItemPacketLoot = 0x66,
|
||||
ItemPacketTrade = 0x67,
|
||||
ItemPacketCharInventory = 0x69,
|
||||
ItemPacketLimbo = 0x6A,
|
||||
ItemPacketWorldContainer = 0x6B,
|
||||
ItemPacketTributeItem = 0x6C,
|
||||
ItemPacketGuildTribute = 0x6D,
|
||||
ItemPacketCharmUpdate = 0x6E, // noted as incorrect
|
||||
ItemPacketInvalid = 0xFF
|
||||
enum ItemPacketType {
|
||||
ItemPacketViewLink = 0x00,
|
||||
ItemPacketMerchant = 0x64,
|
||||
ItemPacketTradeView = 0x65,
|
||||
ItemPacketLoot = 0x66,
|
||||
ItemPacketTrade = 0x67,
|
||||
ItemPacketCharInventory = 0x69,
|
||||
ItemPacketLimbo = 0x6A,
|
||||
ItemPacketWorldContainer = 0x6B,
|
||||
ItemPacketTributeItem = 0x6C,
|
||||
ItemPacketGuildTribute = 0x6D,
|
||||
ItemPacketCharmUpdate = 0x6E, // noted as incorrect
|
||||
ItemPacketRecovery = 0x71,
|
||||
ItemPacketParcel = 0x73,
|
||||
ItemPacketInvalid = 0xFF
|
||||
};
|
||||
|
||||
enum MerchantWindowTabDisplay {
|
||||
None = 0x00,
|
||||
SellBuy = 0x01,
|
||||
Recover = 0x02,
|
||||
SellBuyRecover = 0x03,
|
||||
Parcel = 0x04,
|
||||
SellBuyParcel = 0x05,
|
||||
RecoverParcel = 0x06,
|
||||
SellBuyRecoverParcel = 0x07
|
||||
};
|
||||
|
||||
//enum ItemPacketType
|
||||
@ -2057,12 +2071,75 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
/*008*/ uint32 command; //1=open, 0=cancel/close
|
||||
/*012*/ float rate; //cost multiplier, dosent work anymore
|
||||
struct MerchantClick_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
|
||||
/*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client
|
||||
/*024*/
|
||||
};
|
||||
|
||||
enum MerchantActions {
|
||||
Close = 0,
|
||||
Open = 1
|
||||
};
|
||||
|
||||
struct Parcel_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id;
|
||||
/*004*/ uint32 item_slot;
|
||||
/*008*/ uint32 quantity;
|
||||
/*012*/ uint32 money_flag;
|
||||
/*016*/ char send_to[64];
|
||||
/*080*/ char note[128];
|
||||
/*208*/ uint32 unknown_208;
|
||||
/*212*/ uint32 unknown_212;
|
||||
/*216*/ uint32 unknown_216;
|
||||
};
|
||||
|
||||
struct ParcelRetrieve_Struct
|
||||
{
|
||||
uint32 merchant_entity_id;
|
||||
uint32 player_entity_id;
|
||||
uint32 parcel_slot_id;
|
||||
uint32 parcel_item_id;
|
||||
};
|
||||
|
||||
struct ParcelMessaging_Struct {
|
||||
ItemPacketType packet_type;
|
||||
std::string serialized_item;
|
||||
uint32 sent_time;
|
||||
std::string player_name;
|
||||
std::string note;
|
||||
uint32 slot_id;
|
||||
|
||||
template<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:
|
||||
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
|
||||
|
||||
@ -697,8 +697,11 @@ void PlayerEventLogs::SetSettingsDefaults()
|
||||
m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1;
|
||||
m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1;
|
||||
m_settings[PlayerEvent::ITEM_CREATION].event_enabled = 1;
|
||||
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1;
|
||||
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1;
|
||||
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1;
|
||||
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1;
|
||||
m_settings[PlayerEvent::PARCEL_SEND].event_enabled = 1;
|
||||
m_settings[PlayerEvent::PARCEL_RETRIEVE].event_enabled = 1;
|
||||
m_settings[PlayerEvent::PARCEL_DELETE].event_enabled = 1;
|
||||
|
||||
for (int i = PlayerEvent::GM_COMMAND; i != PlayerEvent::MAX; i++) {
|
||||
m_settings[i].retention_days = RETENTION_DAYS_DEFAULT;
|
||||
|
||||
@ -58,6 +58,9 @@ namespace PlayerEvent {
|
||||
ITEM_CREATION,
|
||||
GUILD_TRIBUTE_DONATE_ITEM,
|
||||
GUILD_TRIBUTE_DONATE_PLAT,
|
||||
PARCEL_SEND,
|
||||
PARCEL_RETRIEVE,
|
||||
PARCEL_DELETE,
|
||||
MAX // dont remove
|
||||
};
|
||||
|
||||
@ -66,7 +69,7 @@ namespace PlayerEvent {
|
||||
// If event is unimplemented just tag (Unimplemented) in the name
|
||||
// Events don't get saved to the database if unimplemented or deprecated
|
||||
// Events tagged as deprecated will get automatically removed
|
||||
static const char *EventName[PlayerEvent::MAX] = {
|
||||
static const char *EventName[EventType::MAX] = {
|
||||
"None",
|
||||
"GM Command",
|
||||
"Zoning",
|
||||
@ -116,7 +119,10 @@ namespace PlayerEvent {
|
||||
"Killed Raid NPC",
|
||||
"Item Creation",
|
||||
"Guild Tribute Donate Item",
|
||||
"Guild Tribute Donate Platinum"
|
||||
"Guild Tribute Donate Platinum",
|
||||
"Parcel Item Sent",
|
||||
"Parcel Item Retrieved",
|
||||
"Parcel Prune Routine"
|
||||
};
|
||||
|
||||
// Generic struct used by all events
|
||||
@ -976,6 +982,68 @@ namespace PlayerEvent {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
struct ParcelRetrieve {
|
||||
uint32 item_id;
|
||||
uint32 quantity;
|
||||
std::string from_player_name;
|
||||
uint32 sent_date;
|
||||
|
||||
// cereal
|
||||
template<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
|
||||
|
||||
@ -3089,21 +3089,6 @@ namespace RoF
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(Merchant_Click_Struct);
|
||||
SETUP_DIRECT_ENCODE(Merchant_Click_Struct, structs::Merchant_Click_Struct);
|
||||
|
||||
OUT(npcid);
|
||||
OUT(playerid);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
eq->unknown01 = 3; // Not sure what these values do yet, but list won't display without them
|
||||
eq->unknown02 = 2592000;
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SkillUpdate)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(SkillUpdate_Struct);
|
||||
@ -5047,19 +5032,6 @@ namespace RoF
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::Merchant_Click_Struct);
|
||||
SETUP_DIRECT_DECODE(Merchant_Click_Struct, structs::Merchant_Click_Struct);
|
||||
|
||||
IN(npcid);
|
||||
IN(playerid);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_Trader)
|
||||
{
|
||||
uint32 psize = __packet->size;
|
||||
|
||||
@ -1579,30 +1579,75 @@ namespace RoF2
|
||||
*p = nullptr;
|
||||
|
||||
//store away the emu struct
|
||||
uchar* __emu_buffer = in->pBuffer;
|
||||
uchar *__emu_buffer = in->pBuffer;
|
||||
ItemPacket_Struct *old_item_pkt = (ItemPacket_Struct *) __emu_buffer;
|
||||
|
||||
ItemPacket_Struct* old_item_pkt = (ItemPacket_Struct*)__emu_buffer;
|
||||
EQ::InternalSerializedItem_Struct* int_struct = (EQ::InternalSerializedItem_Struct*)(&__emu_buffer[4]);
|
||||
switch(old_item_pkt->PacketType)
|
||||
{
|
||||
case ItemPacketParcel: {
|
||||
ParcelMessaging_Struct pms{};
|
||||
EQ::Util::MemoryStreamReader ss(reinterpret_cast<char *>(in->pBuffer), in->size);
|
||||
cereal::BinaryInputArchive ar(ss);
|
||||
ar(pms);
|
||||
|
||||
EQ::OutBuffer ob;
|
||||
EQ::OutBuffer::pos_type last_pos = ob.tellp();
|
||||
uint32 player_name_length = pms.player_name.length();
|
||||
uint32 note_length = pms.note.length();
|
||||
|
||||
ob.write((const char*)__emu_buffer, 4);
|
||||
auto *int_struct = (EQ::InternalSerializedItem_Struct *) pms.serialized_item.data();
|
||||
|
||||
SerializeItem(ob, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType);
|
||||
if (ob.tellp() == last_pos) {
|
||||
LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", int_struct->slot_id);
|
||||
delete in;
|
||||
return;
|
||||
}
|
||||
EQ::OutBuffer ob;
|
||||
EQ::OutBuffer::pos_type last_pos = ob.tellp();
|
||||
ob.write(reinterpret_cast<const char *>(&pms.packet_type), 4);
|
||||
|
||||
in->size = ob.size();
|
||||
in->pBuffer = ob.detach();
|
||||
SerializeItem(ob, (const EQ::ItemInstance *) int_struct->inst, pms.slot_id, 0, ItemPacketParcel);
|
||||
|
||||
delete[] __emu_buffer;
|
||||
if (ob.tellp() == last_pos) {
|
||||
LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]", pms.slot_id);
|
||||
safe_delete_array(__emu_buffer);
|
||||
safe_delete(in);
|
||||
return;
|
||||
}
|
||||
|
||||
dest->FastQueuePacket(&in, ack_req);
|
||||
}
|
||||
ob.write((const char *) &pms.sent_time, 4);
|
||||
ob.write((const char *) &player_name_length, 4);
|
||||
ob.write(pms.player_name.c_str(), pms.player_name.length());
|
||||
ob.write((const char *) ¬e_length, 4);
|
||||
ob.write(pms.note.c_str(), pms.note.length());
|
||||
|
||||
in->size = ob.size();
|
||||
in->pBuffer = ob.detach();
|
||||
|
||||
safe_delete_array(__emu_buffer);
|
||||
dest->FastQueuePacket(&in, ack_req);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
EQ::InternalSerializedItem_Struct *int_struct = (EQ::InternalSerializedItem_Struct *)(&__emu_buffer[4]);
|
||||
|
||||
EQ::OutBuffer ob;
|
||||
EQ::OutBuffer::pos_type last_pos = ob.tellp();
|
||||
|
||||
ob.write((const char *)__emu_buffer, 4);
|
||||
|
||||
SerializeItem(ob, (const EQ::ItemInstance *)int_struct->inst, int_struct->slot_id, 0,
|
||||
old_item_pkt->PacketType);
|
||||
if (ob.tellp() == last_pos) {
|
||||
LogNetcode("RoF2::ENCODE(OP_ItemPacket) Serialization failed on item slot [{}]",
|
||||
int_struct->slot_id);
|
||||
safe_delete_array(__emu_buffer);
|
||||
safe_delete(in);
|
||||
return;
|
||||
}
|
||||
|
||||
in->size = ob.size();
|
||||
in->pBuffer = ob.detach();
|
||||
|
||||
safe_delete_array(__emu_buffer);
|
||||
dest->FastQueuePacket(&in, ack_req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ENCODE(OP_ItemVerifyReply)
|
||||
{
|
||||
@ -3163,21 +3208,6 @@ namespace RoF2
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(Merchant_Click_Struct);
|
||||
SETUP_DIRECT_ENCODE(Merchant_Click_Struct, structs::Merchant_Click_Struct);
|
||||
|
||||
OUT(npcid);
|
||||
OUT(playerid);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
eq->unknown01 = 3; // Not sure what these values do yet, but list won't display without them
|
||||
eq->unknown02 = 2592000;
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SkillUpdate)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(SkillUpdate_Struct);
|
||||
@ -5307,15 +5337,17 @@ namespace RoF2
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
DECODE(OP_ShopSendParcel)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::Merchant_Click_Struct);
|
||||
SETUP_DIRECT_DECODE(Merchant_Click_Struct, structs::Merchant_Click_Struct);
|
||||
DECODE_LENGTH_EXACT(structs::Parcel_Struct);
|
||||
SETUP_DIRECT_DECODE(Parcel_Struct, structs::Parcel_Struct);
|
||||
|
||||
IN(npcid);
|
||||
IN(playerid);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
IN(npc_id);
|
||||
IN(quantity);
|
||||
IN(money_flag);
|
||||
emu->item_slot = RoF2ToServerTypelessSlot(eq->inventory_slot, invtype::typePossessions);
|
||||
strn0cpy(emu->send_to, eq->send_to, sizeof(emu->send_to));
|
||||
strn0cpy(emu->note, eq->note, sizeof(emu->note));
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
@ -5534,11 +5566,24 @@ namespace RoF2
|
||||
//sprintf(hdr.unknown000, "06e0002Y1W00");
|
||||
|
||||
snprintf(hdr.unknown000, sizeof(hdr.unknown000), "%016d", item->ID);
|
||||
if (packet_type == ItemPacketParcel) {
|
||||
strn0cpy(
|
||||
hdr.unknown000,
|
||||
fmt::format(
|
||||
"{:03}PAR{:010}\0",
|
||||
inst->GetMerchantSlot(),
|
||||
item->ID
|
||||
).c_str(),
|
||||
sizeof(hdr.unknown000)
|
||||
);
|
||||
}
|
||||
|
||||
hdr.stacksize = (inst->IsStackable() ? ((inst->GetCharges() > 1000) ? 0xFFFFFFFF : inst->GetCharges()) : 1);
|
||||
hdr.stacksize =
|
||||
item->ID == PARCEL_MONEY_ITEM_ID ? inst->GetPrice() : (inst->IsStackable() ? ((inst->GetCharges() > 1000)
|
||||
? 0xFFFFFFFF : inst->GetCharges()) : 1);
|
||||
hdr.unknown004 = 0;
|
||||
|
||||
structs::InventorySlot_Struct slot_id;
|
||||
structs::InventorySlot_Struct slot_id{};
|
||||
switch (packet_type) {
|
||||
case ItemPacketLoot:
|
||||
slot_id = ServerToRoF2CorpseSlot(slot_id_in);
|
||||
@ -5548,22 +5593,24 @@ namespace RoF2
|
||||
break;
|
||||
}
|
||||
|
||||
hdr.slot_type = (inst->GetMerchantSlot() ? invtype::typeMerchant : slot_id.Type);
|
||||
hdr.main_slot = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : slot_id.Slot);
|
||||
hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex);
|
||||
hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex);
|
||||
hdr.price = inst->GetPrice();
|
||||
hdr.merchant_slot = (inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1);
|
||||
hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0);
|
||||
hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber());
|
||||
hdr.unknown028 = 0;
|
||||
hdr.slot_type = (inst->GetMerchantSlot() ? invtype::typeMerchant : slot_id.Type);
|
||||
hdr.main_slot = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : slot_id.Slot);
|
||||
hdr.sub_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.SubIndex);
|
||||
hdr.aug_slot = (inst->GetMerchantSlot() ? 0xffff : slot_id.AugIndex);
|
||||
hdr.price = inst->GetPrice();
|
||||
hdr.merchant_slot = ((inst->GetMerchantSlot() ? inst->GetMerchantCount() : 1));
|
||||
hdr.scaled_value = (inst->IsScaling() ? (inst->GetExp() / 100) : 0);
|
||||
hdr.instance_id = (inst->GetMerchantSlot() ? inst->GetMerchantSlot() : inst->GetSerialNumber());
|
||||
hdr.parcel_item_id = packet_type == ItemPacketParcel ? inst->GetID() : 0;
|
||||
hdr.last_cast_time = inst->GetRecastTimestamp();
|
||||
hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254) ? 0xFFFFFFFF : inst->GetCharges()));
|
||||
hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0);
|
||||
hdr.unknown044 = 0;
|
||||
hdr.unknown048 = 0;
|
||||
hdr.unknown052 = 0;
|
||||
hdr.isEvolving = item->EvolvingItem;
|
||||
hdr.charges = (inst->IsStackable() ? (item->MaxCharges ? 1 : 0) : ((inst->GetCharges() > 254)
|
||||
? 0xFFFFFFFF
|
||||
: inst->GetCharges()));
|
||||
hdr.inst_nodrop = (inst->IsAttuned() ? 1 : 0);
|
||||
hdr.unknown044 = 0;
|
||||
hdr.unknown048 = 0;
|
||||
hdr.unknown052 = 0;
|
||||
hdr.isEvolving = item->EvolvingItem;
|
||||
|
||||
ob.write((const char*)&hdr, sizeof(RoF2::structs::ItemSerializationHeader));
|
||||
|
||||
@ -5621,9 +5668,10 @@ namespace RoF2
|
||||
|
||||
ob.write((const char*)&hdrf, sizeof(RoF2::structs::ItemSerializationHeaderFinish));
|
||||
|
||||
if (strlen(item->Name) > 0)
|
||||
if (strlen(item->Name) > 0) {
|
||||
ob.write(item->Name, strlen(item->Name));
|
||||
ob.write("\0", 1);
|
||||
ob.write("\0", 1);
|
||||
}
|
||||
|
||||
if (strlen(item->Lore) > 0)
|
||||
ob.write(item->Lore, strlen(item->Lore));
|
||||
@ -5784,10 +5832,11 @@ namespace RoF2
|
||||
itbs.unknown5 = 0;
|
||||
|
||||
itbs.potion_belt_enabled = item->PotionBelt;
|
||||
itbs.potion_belt_slots = item->PotionBeltSlots;
|
||||
itbs.stacksize = (inst->IsStackable() ? item->StackSize : 0);
|
||||
itbs.no_transfer = item->NoTransfer;
|
||||
itbs.expendablearrow = item->ExpendableArrow;
|
||||
itbs.potion_belt_slots = item->PotionBeltSlots;
|
||||
itbs.stacksize =
|
||||
item->ID == PARCEL_MONEY_ITEM_ID ? 0x7FFFFFFF : ((inst->IsStackable() ? item->StackSize : 0));
|
||||
itbs.no_transfer = item->NoTransfer;
|
||||
itbs.expendablearrow = item->ExpendableArrow;
|
||||
|
||||
// Done to hack older clients to label expendable fishing poles as such
|
||||
// July 28th, 2018 patch
|
||||
|
||||
@ -116,7 +116,6 @@ E(OP_SendZonepoints)
|
||||
E(OP_SetGuildRank)
|
||||
E(OP_ShopPlayerBuy)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SkillUpdate)
|
||||
E(OP_SomeItemPacketMaybe)
|
||||
E(OP_SpawnAppearance)
|
||||
@ -200,7 +199,7 @@ D(OP_Save)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerBuy)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_ShopSendParcel)
|
||||
D(OP_Trader)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
|
||||
@ -2247,15 +2247,17 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client
|
||||
/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client
|
||||
/*024*/
|
||||
struct MerchantClick_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
|
||||
/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client
|
||||
/*024*/
|
||||
};
|
||||
|
||||
/*
|
||||
Unknowns:
|
||||
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
|
||||
@ -4572,25 +4574,25 @@ struct RoF2SlotStruct
|
||||
|
||||
struct ItemSerializationHeader
|
||||
{
|
||||
/*000*/ char unknown000[17]; // New for HoT. Looks like a string.
|
||||
/*017*/ uint32 stacksize;
|
||||
/*021*/ uint32 unknown004;
|
||||
/*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ?
|
||||
/*026*/ uint16 main_slot;
|
||||
/*028*/ uint16 sub_slot;
|
||||
/*030*/ uint16 aug_slot; // 0xffff
|
||||
/*032*/ uint32 price;
|
||||
/*036*/ uint32 merchant_slot; //1 if not a merchant item
|
||||
/*040*/ uint32 scaled_value; //0
|
||||
/*044*/ uint32 instance_id; //unique instance id if not merchant item, else is merchant slot
|
||||
/*048*/ uint32 unknown028; //0
|
||||
/*052*/ uint32 last_cast_time; // Unix Time from PP of last cast for this recast type if recast delay > 0
|
||||
/*056*/ uint32 charges; //Total Charges an item has (-1 for unlimited)
|
||||
/*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items)
|
||||
/*064*/ uint32 unknown044; // 0
|
||||
/*068*/ uint32 unknown048; // 0
|
||||
/*072*/ uint32 unknown052; // 0
|
||||
uint8 isEvolving;
|
||||
/*000*/ char unknown000[17]; // New for HoT. Looks like a string.
|
||||
/*017*/ uint32 stacksize;
|
||||
/*021*/ uint32 unknown004;
|
||||
/*025*/ uint8 slot_type; // 0 = normal, 1 = bank, 2 = shared bank, 9 = merchant, 20 = ?
|
||||
/*026*/ uint16 main_slot;
|
||||
/*028*/ uint16 sub_slot;
|
||||
/*030*/ uint16 aug_slot; // 0xffff
|
||||
/*032*/ uint32 price;
|
||||
/*036*/ uint32 merchant_slot; // 1 if not a merchant item
|
||||
/*040*/ uint32 scaled_value; // 0
|
||||
/*044*/ uint32 instance_id; // unique instance id if not merchant item, else is merchant slot
|
||||
/*048*/ uint32 parcel_item_id;
|
||||
/*052*/ uint32 last_cast_time; // Unix Time from PP of last cast for this recast type if recast delay > 0
|
||||
/*056*/ uint32 charges; // Total Charges an item has (-1 for unlimited)
|
||||
/*060*/ uint32 inst_nodrop; // 1 if the item is no drop (attuned items)
|
||||
/*064*/ uint32 unknown044; // 0
|
||||
/*068*/ uint32 unknown048; // 0
|
||||
/*072*/ uint32 unknown052; // 0
|
||||
uint8 isEvolving;
|
||||
};
|
||||
|
||||
struct EvolvingItem {
|
||||
@ -5261,6 +5263,18 @@ struct Checksum_Struct {
|
||||
uint8_t data[2048];
|
||||
};
|
||||
|
||||
struct Parcel_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id;
|
||||
/*004*/ TypelessInventorySlot_Struct inventory_slot;
|
||||
/*012*/ uint32 quantity;
|
||||
/*016*/ uint32 money_flag;
|
||||
/*020*/ char send_to[64];
|
||||
/*084*/ char note[128];
|
||||
/*212*/ uint32 unknown_212;
|
||||
/*216*/ uint32 unknown_216;
|
||||
/*220*/ uint32 unknown_220;
|
||||
};
|
||||
}; /*structs*/
|
||||
|
||||
}; /*RoF2*/
|
||||
|
||||
@ -101,7 +101,6 @@ E(OP_SendZonepoints)
|
||||
E(OP_SetGuildRank)
|
||||
E(OP_ShopPlayerBuy)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SkillUpdate)
|
||||
E(OP_SomeItemPacketMaybe)
|
||||
E(OP_SpawnAppearance)
|
||||
@ -183,7 +182,6 @@ D(OP_Save)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerBuy)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_Trader)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
|
||||
@ -2200,15 +2200,17 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 unknown01; // Seen 3 from Server or -1 from Client
|
||||
/*020*/ int32 unknown02; // Seen 2592000 from Server or -1 from Client
|
||||
/*024*/
|
||||
struct MerchantClick_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
|
||||
/*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client
|
||||
/*024*/
|
||||
};
|
||||
|
||||
/*
|
||||
Unknowns:
|
||||
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
|
||||
|
||||
@ -2026,6 +2026,19 @@ namespace SoD
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(MerchantClick_Struct);
|
||||
SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
OUT(npc_id);
|
||||
OUT(player_id);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SomeItemPacketMaybe)
|
||||
{
|
||||
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow
|
||||
@ -3483,6 +3496,21 @@ namespace SoD
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::MerchantClick_Struct);
|
||||
SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
IN(npc_id);
|
||||
IN(player_id);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
emu->tab_display = 0;
|
||||
emu->unknown020 = 0;
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_TraderBuy)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);
|
||||
|
||||
@ -78,6 +78,7 @@ E(OP_SendCharInfo)
|
||||
E(OP_SendZonepoints)
|
||||
E(OP_ShopPlayerBuy)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SomeItemPacketMaybe)
|
||||
E(OP_SpawnDoor)
|
||||
E(OP_SpecialMesg)
|
||||
@ -141,6 +142,7 @@ D(OP_Save)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerBuy)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
D(OP_TributeItem)
|
||||
|
||||
@ -1845,12 +1845,16 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
/*008*/ uint32 command; //1=open, 0=cancel/close
|
||||
/*012*/ float rate; //cost multiplier, dosent work anymore
|
||||
struct MerchantClick_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
|
||||
/*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client
|
||||
};
|
||||
|
||||
/*
|
||||
Unknowns:
|
||||
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
|
||||
|
||||
@ -1683,6 +1683,19 @@ namespace SoF
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(MerchantClick_Struct);
|
||||
SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
OUT(npc_id);
|
||||
OUT(player_id);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SomeItemPacketMaybe)
|
||||
{
|
||||
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow
|
||||
@ -2874,6 +2887,21 @@ namespace SoF
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::MerchantClick_Struct);
|
||||
SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
IN(npc_id);
|
||||
IN(player_id);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
emu->tab_display = 0;
|
||||
emu->unknown020 = 0;
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_TraderBuy)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);
|
||||
|
||||
@ -72,6 +72,7 @@ E(OP_SendAATable)
|
||||
E(OP_SendCharInfo)
|
||||
E(OP_SendZonepoints)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SomeItemPacketMaybe)
|
||||
E(OP_SpawnDoor)
|
||||
E(OP_SpecialMesg)
|
||||
@ -128,6 +129,7 @@ D(OP_ReadBook)
|
||||
D(OP_Save)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
D(OP_TributeItem)
|
||||
|
||||
@ -1859,12 +1859,16 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
/*008*/ uint32 command; //1=open, 0=cancel/close
|
||||
/*012*/ float rate; //cost multiplier, dosent work anymore
|
||||
struct MerchantClick_Struct
|
||||
{
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; // 1=open, 0=cancel/close
|
||||
/*012*/ float rate; // cost multiplier, dosent work anymore
|
||||
/*016*/ int32 tab_display; // bitmask b000 none, b001 Purchase/Sell, b010 Recover, b100 Parcels
|
||||
/*020*/ int32 unknown020; // Seen 2592000 from Server or -1 from Client
|
||||
};
|
||||
|
||||
/*
|
||||
Unknowns:
|
||||
0 is e7 from 01 to // MAYBE SLOT IN PURCHASE
|
||||
|
||||
@ -1858,6 +1858,19 @@ namespace Titanium
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(MerchantClick_Struct);
|
||||
SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
OUT(npc_id);
|
||||
OUT(player_id);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SpecialMesg)
|
||||
{
|
||||
EQApplicationPacket *in = *p;
|
||||
@ -2875,6 +2888,21 @@ namespace Titanium
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::MerchantClick_Struct);
|
||||
SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
IN(npc_id);
|
||||
IN(player_id);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
emu->tab_display = 0;
|
||||
emu->unknown020 = 0;
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_TraderBuy)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);
|
||||
|
||||
@ -74,6 +74,7 @@ E(OP_SendCharInfo)
|
||||
E(OP_SendAATable)
|
||||
E(OP_SetFace)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SpawnAppearance)
|
||||
E(OP_SpecialMesg)
|
||||
E(OP_TaskDescription)
|
||||
@ -120,6 +121,7 @@ D(OP_RaidInvite)
|
||||
D(OP_ReadBook)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
D(OP_TributeItem)
|
||||
|
||||
@ -1669,9 +1669,9 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
struct MerchantClick_Struct {
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; //1=open, 0=cancel/close
|
||||
/*012*/ float rate; //cost multiplier, dosent work anymore
|
||||
};
|
||||
|
||||
@ -2454,6 +2454,19 @@ namespace UF
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_ShopRequest)
|
||||
{
|
||||
ENCODE_LENGTH_EXACT(MerchantClick_Struct);
|
||||
SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
OUT(npc_id);
|
||||
OUT(player_id);
|
||||
OUT(command);
|
||||
OUT(rate);
|
||||
|
||||
FINISH_ENCODE();
|
||||
}
|
||||
|
||||
ENCODE(OP_SomeItemPacketMaybe)
|
||||
{
|
||||
// This Opcode is not named very well. It is used for the animation of arrows leaving the player's bow
|
||||
@ -4047,6 +4060,21 @@ namespace UF
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_ShopRequest)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::MerchantClick_Struct);
|
||||
SETUP_DIRECT_DECODE(MerchantClick_Struct, structs::MerchantClick_Struct);
|
||||
|
||||
IN(npc_id);
|
||||
IN(player_id);
|
||||
IN(command);
|
||||
IN(rate);
|
||||
emu->tab_display = 0;
|
||||
emu->unknown020 = 0;
|
||||
|
||||
FINISH_DIRECT_DECODE();
|
||||
}
|
||||
|
||||
DECODE(OP_TraderBuy)
|
||||
{
|
||||
DECODE_LENGTH_EXACT(structs::TraderBuy_Struct);
|
||||
|
||||
@ -89,6 +89,7 @@ E(OP_SendZonepoints)
|
||||
E(OP_SetGuildRank)
|
||||
E(OP_ShopPlayerBuy)
|
||||
E(OP_ShopPlayerSell)
|
||||
E(OP_ShopRequest)
|
||||
E(OP_SomeItemPacketMaybe)
|
||||
E(OP_SpawnAppearance)
|
||||
E(OP_SpawnDoor)
|
||||
@ -158,6 +159,7 @@ D(OP_Save)
|
||||
D(OP_SetServerFilter)
|
||||
D(OP_ShopPlayerBuy)
|
||||
D(OP_ShopPlayerSell)
|
||||
D(OP_ShopRequest)
|
||||
D(OP_TraderBuy)
|
||||
D(OP_TradeSkillCombine)
|
||||
D(OP_TributeItem)
|
||||
|
||||
@ -1916,9 +1916,9 @@ struct TimeOfDay_Struct {
|
||||
};
|
||||
|
||||
// Darvik: shopkeeper structs
|
||||
struct Merchant_Click_Struct {
|
||||
/*000*/ uint32 npcid; // Merchant NPC's entity id
|
||||
/*004*/ uint32 playerid;
|
||||
struct MerchantClick_Struct {
|
||||
/*000*/ uint32 npc_id; // Merchant NPC's entity id
|
||||
/*004*/ uint32 player_id;
|
||||
/*008*/ uint32 command; //1=open, 0=cancel/close
|
||||
/*012*/ float rate; //cost multiplier, dosent work anymore
|
||||
};
|
||||
|
||||
463
common/repositories/base/base_character_parcels_repository.h
Normal file
463
common/repositories/base/base_character_parcels_repository.h
Normal 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
|
||||
@ -146,6 +146,7 @@ public:
|
||||
int32_t heroic_strikethrough;
|
||||
int32_t faction_amount;
|
||||
uint8_t keeps_sold_items;
|
||||
uint8_t is_parcel_merchant;
|
||||
};
|
||||
|
||||
static std::string PrimaryKey()
|
||||
@ -283,6 +284,7 @@ public:
|
||||
"heroic_strikethrough",
|
||||
"faction_amount",
|
||||
"keeps_sold_items",
|
||||
"is_parcel_merchant",
|
||||
};
|
||||
}
|
||||
|
||||
@ -416,6 +418,7 @@ public:
|
||||
"heroic_strikethrough",
|
||||
"faction_amount",
|
||||
"keeps_sold_items",
|
||||
"is_parcel_merchant",
|
||||
};
|
||||
}
|
||||
|
||||
@ -583,6 +586,7 @@ public:
|
||||
e.heroic_strikethrough = 0;
|
||||
e.faction_amount = 0;
|
||||
e.keeps_sold_items = 1;
|
||||
e.is_parcel_merchant = 0;
|
||||
|
||||
return e;
|
||||
}
|
||||
@ -746,6 +750,7 @@ public:
|
||||
e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 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.is_parcel_merchant = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 0;
|
||||
|
||||
return e;
|
||||
}
|
||||
@ -905,6 +910,7 @@ public:
|
||||
v.push_back(columns[124] + " = " + std::to_string(e.heroic_strikethrough));
|
||||
v.push_back(columns[125] + " = " + std::to_string(e.faction_amount));
|
||||
v.push_back(columns[126] + " = " + std::to_string(e.keeps_sold_items));
|
||||
v.push_back(columns[127] + " = " + std::to_string(e.is_parcel_merchant));
|
||||
|
||||
auto results = db.QueryDatabase(
|
||||
fmt::format(
|
||||
@ -1053,6 +1059,7 @@ public:
|
||||
v.push_back(std::to_string(e.heroic_strikethrough));
|
||||
v.push_back(std::to_string(e.faction_amount));
|
||||
v.push_back(std::to_string(e.keeps_sold_items));
|
||||
v.push_back(std::to_string(e.is_parcel_merchant));
|
||||
|
||||
auto results = db.QueryDatabase(
|
||||
fmt::format(
|
||||
@ -1209,6 +1216,7 @@ public:
|
||||
v.push_back(std::to_string(e.heroic_strikethrough));
|
||||
v.push_back(std::to_string(e.faction_amount));
|
||||
v.push_back(std::to_string(e.keeps_sold_items));
|
||||
v.push_back(std::to_string(e.is_parcel_merchant));
|
||||
|
||||
insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
|
||||
}
|
||||
@ -1369,6 +1377,7 @@ public:
|
||||
e.heroic_strikethrough = row[124] ? static_cast<int32_t>(atoi(row[124])) : 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.is_parcel_merchant = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 0;
|
||||
|
||||
all_entries.push_back(e);
|
||||
}
|
||||
@ -1520,6 +1529,7 @@ public:
|
||||
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.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);
|
||||
}
|
||||
@ -1721,6 +1731,7 @@ public:
|
||||
v.push_back(std::to_string(e.heroic_strikethrough));
|
||||
v.push_back(std::to_string(e.faction_amount));
|
||||
v.push_back(std::to_string(e.keeps_sold_items));
|
||||
v.push_back(std::to_string(e.is_parcel_merchant));
|
||||
|
||||
auto results = db.QueryDatabase(
|
||||
fmt::format(
|
||||
@ -1870,6 +1881,7 @@ public:
|
||||
v.push_back(std::to_string(e.heroic_strikethrough));
|
||||
v.push_back(std::to_string(e.faction_amount));
|
||||
v.push_back(std::to_string(e.keeps_sold_items));
|
||||
v.push_back(std::to_string(e.is_parcel_merchant));
|
||||
|
||||
insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
|
||||
}
|
||||
|
||||
83
common/repositories/character_parcels_repository.h
Normal file
83
common/repositories/character_parcels_repository.h
Normal 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
|
||||
@ -959,6 +959,16 @@ RULE_BOOL(Items, DisablePotionBelt, false, "Enable this to disable Potion Belt I
|
||||
RULE_BOOL(Items, DisableSpellFocusEffects, false, "Enable this to disable Spell Focus Effects on Items")
|
||||
RULE_CATEGORY_END()
|
||||
|
||||
RULE_CATEGORY(Parcel)
|
||||
RULE_BOOL(Parcel, EnableParcelMerchants, true, "Enable or Disable Parcel Merchants. Requires RoF+ Clients.")
|
||||
RULE_BOOL(Parcel, EnableDirectToInventoryDelivery, false, "Enable or Disable RoF2 bazaar purchases to be delivered directly to the buyer's inventory.")
|
||||
RULE_BOOL(Parcel, DeleteOnDuplicate, false, "Delete retrieved item if it creates a lore conflict.")
|
||||
RULE_BOOL(Parcel, EnablePruning, false, "Enable the automatic pruning of sent parcels. Uses rule ParcelPruneDelay for prune delay.")
|
||||
RULE_INT(Parcel, ParcelDeliveryDelay, 30000, "Sets the time that a player must wait between sending parcels.")
|
||||
RULE_INT(Parcel, ParcelMaxItems, 50, "The maximum number of parcels a player is allowed to have in their mailbox.")
|
||||
RULE_INT(Parcel, ParcelPruneDelay, 30, "The number of days after which a parcel is deleted. Items are lost!")
|
||||
RULE_CATEGORY_END()
|
||||
|
||||
#undef RULE_CATEGORY
|
||||
#undef RULE_INT
|
||||
#undef RULE_REAL
|
||||
|
||||
@ -11,7 +11,7 @@ namespace ServerEvents {
|
||||
static const std::string EVENT_TYPE_RELOAD_WORLD = "reload_world";
|
||||
static const std::string EVENT_TYPE_RULE_CHANGE = "rule_change";
|
||||
static const std::string EVENT_TYPE_CONTENT_FLAG_CHANGE = "content_flag_change";
|
||||
}
|
||||
} // namespace ServerEvents
|
||||
|
||||
class ServerEventScheduler {
|
||||
public:
|
||||
|
||||
@ -113,6 +113,9 @@
|
||||
#define ServerOP_GuildSendGuildList 0x007E
|
||||
#define ServerOP_GuildMembersList 0x007F
|
||||
|
||||
#define ServerOP_ParcelDelivery 0x0090
|
||||
#define ServerOP_ParcelPrune 0x0091
|
||||
|
||||
#define ServerOP_RaidAdd 0x0100 //in use
|
||||
#define ServerOP_RaidRemove 0x0101 //in use
|
||||
#define ServerOP_RaidDisband 0x0102 //in use
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
* Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt
|
||||
*/
|
||||
|
||||
#define CURRENT_BINARY_DATABASE_VERSION 9270
|
||||
#define CURRENT_BINARY_DATABASE_VERSION 9271
|
||||
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9043
|
||||
|
||||
#endif
|
||||
|
||||
@ -32,11 +32,13 @@
|
||||
#include "../common/content/world_content_service.h"
|
||||
#include "../common/zone_store.h"
|
||||
#include "../common/path_manager.h"
|
||||
#include "../common/events/player_event_logs.h"
|
||||
|
||||
EQEmuLogSys LogSys;
|
||||
EQEmuLogSys LogSys;
|
||||
WorldContentService content_service;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
ZoneStore zone_store;
|
||||
PathManager path;
|
||||
PlayerEventLogs player_event_logs;
|
||||
|
||||
#ifdef _WINDOWS
|
||||
#include <direct.h>
|
||||
|
||||
@ -467,6 +467,10 @@ OP_ShopEnd=0x30a8
|
||||
OP_ShopEndConfirm=0x3196
|
||||
OP_ShopPlayerBuy=0x0ddd
|
||||
OP_ShopDelItem=0x724f
|
||||
OP_ShopSendParcel=0x3a5d
|
||||
OP_ShopDeleteParcel=0x47f1
|
||||
OP_ShopRetrieveParcel=0x7013
|
||||
OP_ShopParcelIcon=0x46f0
|
||||
|
||||
# tradeskill stuff:
|
||||
OP_ClickObject=0x4aa1
|
||||
|
||||
@ -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);
|
||||
@ -87,6 +87,7 @@
|
||||
#include "../common/path_manager.h"
|
||||
#include "../common/events/player_event_logs.h"
|
||||
#include "../common/skill_caps.h"
|
||||
#include "../common/repositories/character_parcels_repository.h"
|
||||
|
||||
SkillCaps skill_caps;
|
||||
ZoneStore zone_store;
|
||||
@ -176,6 +177,9 @@ int main(int argc, char **argv)
|
||||
PurgeInstanceTimer.Start(450000);
|
||||
Timer EQTimeTimer(600000);
|
||||
EQTimeTimer.Start(600000);
|
||||
Timer parcel_prune_timer(86400000);
|
||||
parcel_prune_timer.Start(86400000);
|
||||
|
||||
|
||||
// global loads
|
||||
LogInfo("Loading launcher list");
|
||||
@ -420,6 +424,20 @@ int main(int argc, char **argv)
|
||||
client_list.Process();
|
||||
guild_mgr.Process();
|
||||
|
||||
if (parcel_prune_timer.Check()) {
|
||||
if (RuleB(Parcel, EnableParcelMerchants) && RuleB(Parcel, EnablePruning)) {
|
||||
LogTrading(
|
||||
"Parcel Prune process running for parcels over <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()) {
|
||||
player_event_logs.Process();
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "world_event_scheduler.h"
|
||||
#include "../common/servertalk.h"
|
||||
#include <ctime>
|
||||
#include "../common/rulesys.h"
|
||||
|
||||
void WorldEventScheduler::Process(ZSList *zs_list)
|
||||
{
|
||||
@ -31,13 +32,10 @@ void WorldEventScheduler::Process(ZSList *zs_list)
|
||||
);
|
||||
|
||||
for (auto &e: m_events) {
|
||||
|
||||
// discard uninteresting events as its less work to calculate time on events we don't care about
|
||||
// different processes are interested in different events
|
||||
if (
|
||||
e.event_type != ServerEvents::EVENT_TYPE_BROADCAST &&
|
||||
e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD
|
||||
) {
|
||||
if (e.event_type != ServerEvents::EVENT_TYPE_BROADCAST &&
|
||||
e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -57,7 +55,7 @@ void WorldEventScheduler::Process(ZSList *zs_list)
|
||||
if (e.event_type == ServerEvents::EVENT_TYPE_RELOAD_WORLD) {
|
||||
LogScheduler("Sending reload world event [{}]", e.event_data.c_str());
|
||||
|
||||
auto pack = new ServerPacket(ServerOP_ReloadWorld, sizeof(ReloadWorld_Struct));
|
||||
auto pack = new ServerPacket(ServerOP_ReloadWorld, sizeof(ReloadWorld_Struct));
|
||||
auto *reload_world = (ReloadWorld_Struct *) pack->pBuffer;
|
||||
reload_world->global_repop = ReloadWorld::Repop;
|
||||
zs_list->SendPacket(pack);
|
||||
|
||||
@ -1730,6 +1730,19 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ServerOP_ParcelDelivery: {
|
||||
auto in = (Parcel_Struct *) pack->pBuffer;
|
||||
if (strlen(in->send_to) == 0) {
|
||||
LogError(
|
||||
"ServerOP_ParcelDelivery pack received with invalid character name of [{}]",
|
||||
in->send_to);
|
||||
return;
|
||||
}
|
||||
|
||||
zoneserver_list.SendPacketToBootedZones(pack);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size);
|
||||
DumpPacket(pack->pBuffer, pack->size);
|
||||
|
||||
@ -101,6 +101,7 @@ SET(zone_sources
|
||||
npc_scale_manager.cpp
|
||||
object.cpp
|
||||
oriented_bounding_box.cpp
|
||||
parcels.cpp
|
||||
pathfinder_interface.cpp
|
||||
pathfinder_nav_mesh.cpp
|
||||
pathfinder_null.cpp
|
||||
|
||||
@ -184,7 +184,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
|
||||
mob_close_scan_timer(6000),
|
||||
position_update_timer(10000),
|
||||
consent_throttle_timer(2000),
|
||||
tmSitting(0)
|
||||
tmSitting(0),
|
||||
parcel_timer(RuleI(Parcel, ParcelDeliveryDelay))
|
||||
{
|
||||
for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) {
|
||||
SetFilter(client_filter, FilterShow);
|
||||
@ -375,6 +376,15 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
|
||||
bot_owner_options[booBuffCounter] = false;
|
||||
bot_owner_options[booMonkWuMessage] = false;
|
||||
|
||||
m_parcel_platinum = 0;
|
||||
m_parcel_gold = 0;
|
||||
m_parcel_silver = 0;
|
||||
m_parcel_copper = 0;
|
||||
m_parcel_count = 0;
|
||||
m_parcel_enabled = true;
|
||||
m_parcel_merchant_engaged = false;
|
||||
m_parcels.clear();
|
||||
|
||||
SetBotPulling(false);
|
||||
SetBotPrecombat(false);
|
||||
|
||||
@ -383,6 +393,10 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
|
||||
}
|
||||
|
||||
Client::~Client() {
|
||||
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) {
|
||||
DoParcelCancel();
|
||||
}
|
||||
|
||||
mMovementManager->RemoveClient(this);
|
||||
|
||||
DataBucket::DeleteCachedBuckets(DataBucketLoadType::Client, CharacterID());
|
||||
|
||||
@ -68,6 +68,7 @@ namespace EQ
|
||||
#include "cheat_manager.h"
|
||||
#include "../common/events/player_events.h"
|
||||
#include "../common/data_verification.h"
|
||||
#include "../common/repositories/character_parcels_repository.h"
|
||||
|
||||
#ifdef _WINDOWS
|
||||
// since windows defines these within windef.h (which windows.h include)
|
||||
@ -323,8 +324,36 @@ public:
|
||||
void ReturnTraderReq(const EQApplicationPacket* app,int16 traderitemcharges, uint32 itemid = 0);
|
||||
void TradeRequestFailed(const EQApplicationPacket* app);
|
||||
void BuyTraderItem(TraderBuy_Struct* tbs,Client* trader,const EQApplicationPacket* app);
|
||||
void FinishTrade(Mob* with, bool finalizer = false, void* event_entry = nullptr, std::list<void*>* event_details = nullptr);
|
||||
void FinishTrade(
|
||||
Mob *with,
|
||||
bool finalizer = false,
|
||||
void *event_entry = nullptr,
|
||||
std::list<void *> *event_details = nullptr
|
||||
);
|
||||
void SendZonePoints();
|
||||
void SendBulkParcels();
|
||||
void DoParcelCancel();
|
||||
void DoParcelSend(const Parcel_Struct *parcel_in);
|
||||
void DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in);
|
||||
void SendParcel(const Parcel_Struct &parcel);
|
||||
void SendParcelStatus();
|
||||
void SendParcelAck();
|
||||
void SendParcelRetrieveAck();
|
||||
void SendParcelDelete(const ParcelRetrieve_Struct &parcel_in);
|
||||
void SendParcelDeliveryToWorld(const Parcel_Struct &parcel);
|
||||
void SetParcelEnabled(bool status) { m_parcel_enabled = status; }
|
||||
bool GetParcelEnabled() { return m_parcel_enabled; }
|
||||
void SetParcelCount(uint32 count) { m_parcel_count = count; }
|
||||
int32 GetParcelCount() { return m_parcel_count; }
|
||||
bool GetEngagedWithParcelMerchant() { return m_parcel_merchant_engaged; }
|
||||
void SetEngagedWithParcelMerchant(bool status) { m_parcel_merchant_engaged = status; }
|
||||
Timer *GetParcelTimer() { return &parcel_timer; }
|
||||
bool DeleteParcel(uint32 parcel_id);
|
||||
void AddParcel(CharacterParcelsRepository::CharacterParcels &parcel);
|
||||
void LoadParcels();
|
||||
std::map<uint32, CharacterParcelsRepository::CharacterParcels> GetParcels() { return m_parcels; }
|
||||
int32 FindNextFreeParcelSlot(uint32 char_id);
|
||||
void SendParcelIconStatus();
|
||||
|
||||
void SendBuyerResults(char *SearchQuery, uint32 SearchID);
|
||||
void ShowBuyLines(const EQApplicationPacket *app);
|
||||
@ -1857,6 +1886,14 @@ private:
|
||||
bool Trader;
|
||||
bool Buyer;
|
||||
std::string BuyerWelcomeMessage;
|
||||
int32 m_parcel_platinum;
|
||||
int32 m_parcel_gold;
|
||||
int32 m_parcel_silver;
|
||||
int32 m_parcel_copper;
|
||||
int32 m_parcel_count;
|
||||
bool m_parcel_enabled;
|
||||
bool m_parcel_merchant_engaged;
|
||||
std::map<uint32, CharacterParcelsRepository::CharacterParcels> m_parcels{};
|
||||
int Haste; //precalced value
|
||||
uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004
|
||||
|
||||
@ -1955,6 +1992,7 @@ private:
|
||||
Timer dynamiczone_removal_timer;
|
||||
Timer task_request_timer;
|
||||
Timer pick_lock_timer;
|
||||
Timer parcel_timer; //Used to limit the number of parcels to one every 30 seconds (default). Changable via rule.
|
||||
|
||||
glm::vec3 m_Proximity;
|
||||
glm::vec4 last_position_before_bulk_update;
|
||||
|
||||
@ -69,6 +69,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#include "../common/events/player_event_logs.h"
|
||||
#include "../common/repositories/character_stats_record_repository.h"
|
||||
#include "dialogue_window.h"
|
||||
#include "../common/rulesys.h"
|
||||
|
||||
extern QueryServ* QServ;
|
||||
extern Zone* zone;
|
||||
@ -321,6 +322,8 @@ void MapOpcodes()
|
||||
ConnectedOpcodes[OP_OpenGuildTributeMaster] = &Client::Handle_OP_OpenGuildTributeMaster;
|
||||
ConnectedOpcodes[OP_OpenInventory] = &Client::Handle_OP_OpenInventory;
|
||||
ConnectedOpcodes[OP_OpenTributeMaster] = &Client::Handle_OP_OpenTributeMaster;
|
||||
ConnectedOpcodes[OP_ShopSendParcel] = &Client::Handle_OP_ShopSendParcel;
|
||||
ConnectedOpcodes[OP_ShopRetrieveParcel] = &Client::Handle_OP_ShopRetrieveParcel;
|
||||
ConnectedOpcodes[OP_PDeletePetition] = &Client::Handle_OP_PDeletePetition;
|
||||
ConnectedOpcodes[OP_PetCommands] = &Client::Handle_OP_PetCommands;
|
||||
ConnectedOpcodes[OP_Petition] = &Client::Handle_OP_Petition;
|
||||
@ -818,6 +821,10 @@ void Client::CompleteConnect()
|
||||
);
|
||||
}
|
||||
|
||||
if(ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) {
|
||||
SendParcelStatus();
|
||||
}
|
||||
|
||||
if (zone && zone->GetInstanceTimer()) {
|
||||
bool is_permanent = false;
|
||||
uint32 remaining_time = database.GetTimeRemainingInstance(zone->GetInstanceID(), is_permanent);
|
||||
@ -4271,6 +4278,12 @@ void Client::Handle_OP_Bug(const EQApplicationPacket *app)
|
||||
|
||||
void Client::Handle_OP_Camp(const EQApplicationPacket *app)
|
||||
{
|
||||
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants) && GetEngagedWithParcelMerchant()) {
|
||||
Stand();
|
||||
MessageString(Chat::Yellow, TRADER_BUSY_TWO);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsLFP())
|
||||
worldserver.StopLFP(CharacterID());
|
||||
|
||||
@ -4324,6 +4337,12 @@ void Client::Handle_OP_CancelTrade(const EQApplicationPacket *app)
|
||||
FinishTrade(this);
|
||||
trade->Reset();
|
||||
}
|
||||
|
||||
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) {
|
||||
DoParcelCancel();
|
||||
SetEngagedWithParcelMerchant(false);
|
||||
}
|
||||
|
||||
EQApplicationPacket end_trade1(OP_FinishWindow, 0);
|
||||
QueuePacket(&end_trade1);
|
||||
|
||||
@ -13877,6 +13896,11 @@ void Client::Handle_OP_Shielding(const EQApplicationPacket *app)
|
||||
|
||||
void Client::Handle_OP_ShopEnd(const EQApplicationPacket *app)
|
||||
{
|
||||
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB(Parcel, EnableParcelMerchants)) {
|
||||
DoParcelCancel();
|
||||
SetEngagedWithParcelMerchant(false);
|
||||
}
|
||||
|
||||
EQApplicationPacket empty(OP_ShopEndConfirm);
|
||||
QueuePacket(&empty);
|
||||
return;
|
||||
@ -14423,82 +14447,107 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app)
|
||||
|
||||
void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app)
|
||||
{
|
||||
if (app->size != sizeof(Merchant_Click_Struct)) {
|
||||
LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(Merchant_Click_Struct));
|
||||
if (app->size != sizeof(MerchantClick_Struct)) {
|
||||
LogError("Wrong size: OP_ShopRequest, size=[{}], expected [{}]", app->size, sizeof(MerchantClick_Struct));
|
||||
return;
|
||||
}
|
||||
|
||||
Merchant_Click_Struct* mc = (Merchant_Click_Struct*)app->pBuffer;
|
||||
MerchantClick_Struct *mc = (MerchantClick_Struct *) app->pBuffer;
|
||||
|
||||
// Send back opcode OP_ShopRequest - tells client to open merchant window.
|
||||
//EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct));
|
||||
//Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer;
|
||||
int merchantid = 0;
|
||||
Mob* tmp = entity_list.GetMob(mc->npcid);
|
||||
// EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct));
|
||||
// Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer;
|
||||
int merchant_id = 0;
|
||||
int tabs_to_display = None;
|
||||
Mob *tmp = entity_list.GetMob(mc->npc_id);
|
||||
|
||||
if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant)
|
||||
if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != Class::Merchant) {
|
||||
return;
|
||||
}
|
||||
|
||||
//you have to be somewhat close to them to be properly using them
|
||||
if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2)
|
||||
// you have to be somewhat close to them to be properly using them
|
||||
if (DistanceSquared(m_Position, tmp->GetPosition()) > USE_NPC_RANGE2) {
|
||||
return;
|
||||
}
|
||||
|
||||
merchantid = tmp->CastToNPC()->MerchantType;
|
||||
merchant_id = tmp->CastToNPC()->MerchantType;
|
||||
|
||||
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && tmp->CastToNPC()->GetParcelMerchant()) {
|
||||
tabs_to_display = SellBuyParcel;
|
||||
}
|
||||
else {
|
||||
tabs_to_display = SellBuy;
|
||||
}
|
||||
|
||||
int action = MerchantActions::Open;
|
||||
if (merchant_id == 0) {
|
||||
auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(MerchantClick_Struct));
|
||||
auto mco = (MerchantClick_Struct *) outapp->pBuffer;
|
||||
mco->npc_id = mc->npc_id;
|
||||
mco->player_id = 0;
|
||||
mco->command = MerchantActions::Open;
|
||||
mco->rate = 1.0;
|
||||
mco->tab_display = tabs_to_display;
|
||||
|
||||
int action = 1;
|
||||
if (merchantid == 0) {
|
||||
auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct));
|
||||
Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer;
|
||||
mco->npcid = mc->npcid;
|
||||
mco->playerid = 0;
|
||||
mco->command = 1; //open...
|
||||
mco->rate = 1.0;
|
||||
QueuePacket(outapp);
|
||||
safe_delete(outapp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tmp->IsEngaged()) {
|
||||
MessageString(Chat::White, MERCHANT_BUSY);
|
||||
action = 0;
|
||||
}
|
||||
if (GetFeigned() || IsInvisible())
|
||||
{
|
||||
Message(Chat::White, "You cannot use a merchant right now.");
|
||||
action = 0;
|
||||
}
|
||||
int primaryfaction = tmp->CastToNPC()->GetPrimaryFaction();
|
||||
int factionlvl = GetFactionLevel(CharacterID(), tmp->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(), primaryfaction, tmp);
|
||||
if (factionlvl >= 7) {
|
||||
MerchantRejectMessage(tmp, primaryfaction);
|
||||
action = 0;
|
||||
action = MerchantActions::Close;
|
||||
}
|
||||
|
||||
if (tmp->Charmed())
|
||||
action = 0;
|
||||
if (GetFeigned() || IsInvisible()) {
|
||||
Message(Chat::White, "You cannot use a merchant right now.");
|
||||
action = MerchantActions::Close;
|
||||
}
|
||||
|
||||
int primaryfaction = tmp->CastToNPC()->GetPrimaryFaction();
|
||||
int factionlvl = GetFactionLevel(
|
||||
CharacterID(), tmp->CastToNPC()->GetNPCTypeID(), GetRace(), GetClass(), GetDeity(),
|
||||
primaryfaction, tmp
|
||||
);
|
||||
if (factionlvl >= 7) {
|
||||
MerchantRejectMessage(tmp, primaryfaction);
|
||||
action = MerchantActions::Close;
|
||||
}
|
||||
|
||||
if (tmp->Charmed()) {
|
||||
action = MerchantActions::Close;
|
||||
}
|
||||
|
||||
if (!tmp->CastToNPC()->IsMerchantOpen()) {
|
||||
tmp->SayString(zone->random.Int(MERCHANT_CLOSED_ONE, MERCHANT_CLOSED_THREE));
|
||||
action = 0;
|
||||
action = MerchantActions::Close;
|
||||
}
|
||||
|
||||
auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct));
|
||||
Merchant_Click_Struct* mco = (Merchant_Click_Struct*)outapp->pBuffer;
|
||||
auto outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(MerchantClick_Struct));
|
||||
auto mco = (MerchantClick_Struct *) outapp->pBuffer;
|
||||
|
||||
mco->npc_id = mc->npc_id;
|
||||
mco->player_id = 0;
|
||||
mco->command = action; // Merchant command 0x01 = open
|
||||
mco->tab_display = tabs_to_display;
|
||||
|
||||
mco->npcid = mc->npcid;
|
||||
mco->playerid = 0;
|
||||
mco->command = action; // Merchant command 0x01 = open
|
||||
if (RuleB(Merchant, UsePriceMod)) {
|
||||
mco->rate = 1 / ((RuleR(Merchant, BuyCostMod))*Client::CalcPriceMod(tmp, true)); // works
|
||||
mco->rate = 1 / ((RuleR(Merchant, BuyCostMod)) * Client::CalcPriceMod(tmp, true)); // works
|
||||
}
|
||||
else
|
||||
else {
|
||||
mco->rate = 1 / (RuleR(Merchant, BuyCostMod));
|
||||
}
|
||||
|
||||
outapp->priority = 6;
|
||||
QueuePacket(outapp);
|
||||
safe_delete(outapp);
|
||||
|
||||
if (action == 1)
|
||||
BulkSendMerchantInventory(merchantid, tmp->GetNPCTypeID());
|
||||
if (action == MerchantActions::Open) {
|
||||
BulkSendMerchantInventory(merchant_id, tmp->GetNPCTypeID());
|
||||
if ((tabs_to_display & Parcel) == Parcel) {
|
||||
SendBulkParcels();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@ -17162,3 +17211,26 @@ void Client::Handle_OP_GuildTributeDonatePlat(const EQApplicationPacket *app)
|
||||
}
|
||||
}
|
||||
|
||||
void Client::Handle_OP_ShopSendParcel(const EQApplicationPacket *app)
|
||||
{
|
||||
if (app->size != sizeof(Parcel_Struct)) {
|
||||
LogError("Received Handle_OP_ShopSendParcel packet. Expected size {}, received size {}.", sizeof(Parcel_Struct),
|
||||
app->size);
|
||||
return;
|
||||
}
|
||||
|
||||
auto parcel_in = (Parcel_Struct *)app->pBuffer;
|
||||
DoParcelSend(parcel_in);
|
||||
}
|
||||
|
||||
void Client::Handle_OP_ShopRetrieveParcel(const EQApplicationPacket *app)
|
||||
{
|
||||
if (app->size != sizeof(ParcelRetrieve_Struct)) {
|
||||
LogError("Received Handle_OP_ShopRetrieveParcel packet. Expected size {}, received size {}.",
|
||||
sizeof(ParcelRetrieve_Struct), app->size);
|
||||
return;
|
||||
}
|
||||
|
||||
auto parcel_in = (ParcelRetrieve_Struct *)app->pBuffer;
|
||||
DoParcelRetrieve(*parcel_in);
|
||||
}
|
||||
|
||||
@ -335,3 +335,6 @@
|
||||
void Handle_OP_SharedTaskAccept(const EQApplicationPacket *app);
|
||||
void Handle_OP_SharedTaskQuit(const EQApplicationPacket *app);
|
||||
void Handle_OP_SharedTaskPlayerList(const EQApplicationPacket *app);
|
||||
|
||||
void Handle_OP_ShopSendParcel(const EQApplicationPacket *app);
|
||||
void Handle_OP_ShopRetrieveParcel(const EQApplicationPacket *app);
|
||||
|
||||
@ -555,6 +555,7 @@ bool Client::Process() {
|
||||
guild_mgr.UpdateDbMemberOnline(CharacterID(), false);
|
||||
guild_mgr.SendToWorldSendGuildMembersList(GuildID());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else if (!linkdead_timer.Enabled()) {
|
||||
@ -1429,6 +1430,22 @@ void Client::OPMoveCoin(const EQApplicationPacket* app)
|
||||
to_bucket = (int32 *) &trade->cp; break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (mc->cointype2) {
|
||||
case COINTYPE_PP:
|
||||
m_parcel_platinum += mc->amount;
|
||||
break;
|
||||
case COINTYPE_GP:
|
||||
m_parcel_gold += mc->amount;
|
||||
break;
|
||||
case COINTYPE_SP:
|
||||
m_parcel_silver += mc->amount;
|
||||
break;
|
||||
case COINTYPE_CP:
|
||||
m_parcel_copper += mc->amount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: // shared bank
|
||||
|
||||
@ -182,6 +182,7 @@ int command_init(void)
|
||||
command_add("nukeitem", "[Item ID] - Removes the specified Item ID from you or your player target's inventory", AccountStatus::GMLeadAdmin, command_nukeitem) ||
|
||||
command_add("object", "List|Add|Edit|Move|Rotate|Copy|Save|Undo|Delete - Manipulate static and tradeskill objects within the zone", AccountStatus::GMAdmin, command_object) ||
|
||||
command_add("opcode", "Reloads all opcodes from server patch files", AccountStatus::GMMgmt, command_reload) ||
|
||||
command_add("parcels", "View and edit the parcel system. Requires parcels to be enabled in rules.", AccountStatus::GMMgmt, command_parcels) ||
|
||||
command_add("path", "view and edit pathing", AccountStatus::GMMgmt, command_path) ||
|
||||
command_add("peqzone", "[Zone ID|Zone Short Name] - Teleports you to the specified zone if you meet the requirements.", AccountStatus::Player, command_peqzone) ||
|
||||
command_add("petitems", "View your pet's items if you have one", AccountStatus::ApprenticeGuide, command_petitems) ||
|
||||
@ -873,6 +874,7 @@ void command_bot(Client *c, const Seperator *sep)
|
||||
#include "gm_commands/nukeitem.cpp"
|
||||
#include "gm_commands/object.cpp"
|
||||
#include "gm_commands/object_manipulation.cpp"
|
||||
#include "gm_commands/parcels.cpp"
|
||||
#include "gm_commands/path.cpp"
|
||||
#include "gm_commands/peqzone.cpp"
|
||||
#include "gm_commands/petitems.cpp"
|
||||
|
||||
@ -37,6 +37,7 @@ void SendGuildSubCommands(Client *c);
|
||||
void SendShowInventorySubCommands(Client *c);
|
||||
void SendFixMobSubCommands(Client *c);
|
||||
void SendDataBucketsSubCommands(Client *c);
|
||||
void SendParcelsSubCommands(Client *c);
|
||||
|
||||
// Commands
|
||||
void command_acceptrules(Client *c, const Seperator *sep);
|
||||
@ -135,6 +136,7 @@ void command_nukebuffs(Client *c, const Seperator *sep);
|
||||
void command_nukeitem(Client *c, const Seperator *sep);
|
||||
void command_object(Client *c, const Seperator *sep);
|
||||
void command_oocmute(Client *c, const Seperator *sep);
|
||||
void command_parcels(Client *c, const Seperator *sep);
|
||||
void command_path(Client *c, const Seperator *sep);
|
||||
void command_peqzone(Client *c, const Seperator *sep);
|
||||
void command_petitems(Client *c, const Seperator *sep);
|
||||
|
||||
307
zone/gm_commands/parcels.cpp
Normal file
307
zone/gm_commands/parcels.cpp
Normal 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]");
|
||||
}
|
||||
@ -268,6 +268,7 @@ public:
|
||||
inline void MerchantOpenShop() { merchant_open = true; }
|
||||
inline void MerchantCloseShop() { merchant_open = false; }
|
||||
inline bool IsMerchantOpen() { return merchant_open; }
|
||||
inline bool GetParcelMerchant() { return NPCTypedata->is_parcel_merchant; }
|
||||
void Depop(bool start_spawn_timer = false);
|
||||
void Stun(int duration);
|
||||
void UnStun();
|
||||
|
||||
752
zone/parcels.cpp
Normal file
752
zone/parcels.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -192,6 +192,10 @@
|
||||
#define PET_SPELLHOLD_SET_ON 702 //The pet spellhold mode has been set to on.
|
||||
#define PET_SPELLHOLD_SET_OFF 703 //The pet spellhold mode has been set to off.
|
||||
#define GUILD_NAME_IN_USE 711 //You cannot create a guild with that name, that guild already exists on this server.
|
||||
#define PARCEL_DELAY 734 //%1 tells you, 'You must give me a chance to send the last parcel before I can send another!'
|
||||
#define PARCEL_DUPLICATE_DELETE 737 //Duplicate lore items are not allowed! Your duplicate %1 has been deleted!
|
||||
#define PARCEL_DELIVER_3 741 //%1 told you, 'I will deliver the stack of %2 %3 to %4 as soon as possible!'
|
||||
#define PARCEL_INV_FULL 790 //%1 tells you, 'Your inventory appears full! Unable to retrieve parceled item.'
|
||||
#define AA_CAP 1000 //You have reached the AA point cap, and cannot gain any further experience until some of your stored AA point pool is used.
|
||||
#define GM_GAINXP 1002 //[GM] You have gained %1 AXP and %2 EXP (%3).
|
||||
#define MALE_SLAYUNDEAD 1007 //%1's holy blade cleanses his target!(%2)
|
||||
@ -277,6 +281,7 @@
|
||||
#define SPARKLES 1236 //Your %1 sparkles.
|
||||
#define GROWS_DIM 1237 //Your %1 grows dim.
|
||||
#define BEGINS_TO_SHINE 1238 //Your %1 begins to shine.
|
||||
#define CANT_FIND_PLAYER 1276 //I can't find a player named %1!
|
||||
#define SURNAME_REJECTED 1374 //Your new surname was rejected. Please try a different name.
|
||||
#define GUILD_DISBANDED 1377 //Your guild has been disbanded! You are no longer a member of any guild.
|
||||
#define DUEL_DECLINE 1383 //%1 has declined your challenge to duel to the death.
|
||||
@ -302,6 +307,7 @@
|
||||
#define SENSE_CORPSE_DIRECTION 1563 //You sense a corpse in this direction.
|
||||
#define QUEUED_TELL 2458 //[queued]
|
||||
#define QUEUE_TELL_FULL 2459 //[zoing and queue is full]
|
||||
#define TRADER_BUSY_TWO 3192 //Sorry, that action cannot be performed while trading.
|
||||
#define SUSPEND_MINION_UNSUSPEND 3267 //%1 tells you, 'I live again...'
|
||||
#define SUSPEND_MINION_SUSPEND 3268 //%1 tells you, 'By your command, master.'
|
||||
#define ONLY_SUMMONED_PETS 3269 //3269 This effect only works with summoned pets.
|
||||
@ -384,6 +390,8 @@
|
||||
#define ALREADY_IN_GRP_RAID 5088 //% 1 rejects your invite because they are in a raid and you are not in theirs, or they are a raid group leader
|
||||
#define DUNGEON_SEALED 5141 //The gateway to the dungeon is sealed off to you. Perhaps you would be able to enter if you needed to adventure there.
|
||||
#define ADVENTURE_COMPLETE 5147 //You received %1 points for successfully completing the adventure.
|
||||
#define PARCEL_STATUS_2 5433 //You currently have % 1 parcels in your mail and are % 2 parcels over the limit of % 3!If you do not retrieve at least % 2 parcels before you logout, they will be lost!
|
||||
#define PARCEL_STATUS_1 5434 //You currently have % 1 parcels in your mail and are 1 parcel over the limit of % 2!If you do not retrieve at least 1 parcel before you logout, it will be lost!
|
||||
#define SUCCOR_FAIL 5169 //The portal collapes before you can escape!
|
||||
#define NO_PROPER_ACCESS 5410 //You don't have the proper access rights.
|
||||
#define AUGMENT_RESTRICTED 5480 //The item does not satisfy the augment's restrictions.
|
||||
@ -411,6 +419,11 @@
|
||||
#define TRANSFORM_COMPLETE 6327 //You have successfully transformed your %1.
|
||||
#define DETRANSFORM_FAILED 6341 //%1 has no transformation that can be removed.
|
||||
#define GUILD_PERMISSION_FAILED 6418 //You do not have permission to change access options.
|
||||
#define PARCEL_DELIVERY_ARRIVED 6465 //You have received a new parcel delivery!
|
||||
#define PARCEL_DELIVERY 6466 //%1 tells you, 'I will deliver the %2 to %3 as soon as possible!'
|
||||
#define PARCEL_UNKNOWN_NAME 6467 //%1 tells you, 'Unfortunately, I don't know anyone by the name of %2. Here is your %3 back.''
|
||||
#define PARCEL_DELIVERED 6471 //%1 hands you the %2 that was sent from %3.
|
||||
#define PARCEL_DELIVERED_2 6472 //%1 hands you the stack of %2 %3 that was sent from %4.
|
||||
#define GENERIC_STRING 6688 //%1 (used to any basic message)
|
||||
#define SENTINEL_TRIG_YOU 6724 //You have triggered your sentinel.
|
||||
#define SENTINEL_TRIG_OTHER 6725 //%1 has triggered your sentinel.
|
||||
|
||||
@ -3859,6 +3859,51 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ServerOP_ParcelDelivery: {
|
||||
auto in = (Parcel_Struct *) pack->pBuffer;
|
||||
|
||||
if (strlen(in->send_to) == 0) {
|
||||
LogError(
|
||||
"ServerOP_ParcelDelivery pack received with incorrect character_name of {}.",
|
||||
in->send_to
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto const &c: entity_list.GetClientList()) {
|
||||
if (strcasecmp(c.second->GetCleanName(), in->send_to) == 0) {
|
||||
c.second->MessageString(
|
||||
Chat::Yellow,
|
||||
PARCEL_DELIVERY_ARRIVED
|
||||
);
|
||||
c.second->SendParcelStatus();
|
||||
if (c.second->GetEngagedWithParcelMerchant()) {
|
||||
c.second->SendParcel(*in);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ServerOP_ParcelPrune: {
|
||||
for (auto const &c: entity_list.GetClientList()) {
|
||||
if (c.second->GetEngagedWithParcelMerchant()) {
|
||||
c.second->Message(
|
||||
Chat::Red,
|
||||
"Parcel data has been updated. Please re-open the Merchant Window."
|
||||
);
|
||||
c.second->SetEngagedWithParcelMerchant(false);
|
||||
c.second->DoParcelCancel();
|
||||
|
||||
auto out = new EQApplicationPacket(OP_ShopEndConfirm);
|
||||
c.second->QueuePacket(out);
|
||||
safe_delete(out);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
LogInfo("Unknown ZS Opcode [{}] size [{}]", (int)pack->opcode, pack->size);
|
||||
break;
|
||||
|
||||
@ -1792,6 +1792,7 @@ const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load
|
||||
t->min_dmg = n.mindmg;
|
||||
t->max_dmg = n.maxdmg;
|
||||
t->attack_count = n.attack_count;
|
||||
t->is_parcel_merchant = n.is_parcel_merchant ? true : false;
|
||||
|
||||
if (!n.special_abilities.empty()) {
|
||||
strn0cpy(t->special_abilities, n.special_abilities.c_str(), 512);
|
||||
|
||||
@ -154,6 +154,7 @@ struct NPCType
|
||||
int exp_mod;
|
||||
int heroic_strikethrough;
|
||||
bool keeps_sold_items;
|
||||
bool is_parcel_merchant;
|
||||
};
|
||||
|
||||
#pragma pack()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user