From ca704c7f881fb6f81a7304bdb84b64227122ad46 Mon Sep 17 00:00:00 2001 From: Dan <3968021+sh0ber@users.noreply.github.com> Date: Wed, 13 May 2026 03:18:36 -0400 Subject: [PATCH] feat(Scripting): Created two perl/lua scripting hooks for merchants (#5083) --- zone/client_packet.cpp | 55 +++++++++++++++++++++++++++----------- zone/embparser.cpp | 29 ++++++++++++++++++++ zone/event_codes.h | 2 ++ zone/lua_general.cpp | 2 ++ zone/lua_iteminst.cpp | 6 +++++ zone/lua_iteminst.h | 1 + zone/lua_parser.cpp | 4 +++ zone/lua_parser_events.cpp | 47 ++++++++++++++++++++++++++++++++ zone/lua_parser_events.h | 18 +++++++++++++ 9 files changed, 148 insertions(+), 16 deletions(-) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 13e55df4d..f4266afb4 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -14378,9 +14378,8 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) sizeof(Merchant_Purchase_Struct), app->size); return; } - RDTSC_Timer t1(true); + Merchant_Purchase_Struct* mp = (Merchant_Purchase_Struct*)app->pBuffer; - Mob* vendor = entity_list.GetMob(mp->npcid); if (vendor == 0 || !vendor->IsNPC() || vendor->GetClass() != Class::Merchant) @@ -14390,35 +14389,51 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) if (DistanceSquared(m_Position, vendor->GetPosition()) > USE_NPC_RANGE2) return; - uint32 price = 0; uint32 itemid = GetItemIDAt(mp->itemslot); if (itemid == 0) return; + const EQ::ItemData* item = database.GetItem(itemid); EQ::ItemInstance* inst = GetInv().GetItem(mp->itemslot); if (!item || !inst) { - Message(Chat::Red, "You seemed to have misplaced that item.."); + Message(Chat::Red, "You seem to have misplaced that item.."); return; } - if (mp->quantity > 1) - { + + if (!item->NoDrop) { + return; + } + + if (mp->quantity > 1) { if ((inst->GetCharges() < 0) || (mp->quantity > (uint32)inst->GetCharges())) return; } - if (!item->NoDrop) { - //Message(Chat::Red,"%s tells you, 'LOL NOPE'", vendor->GetName()); - return; + // Check for veto from script + if (parse->PlayerHasQuestSub(EVENT_MERCHANT_PRESELL)) { + std::string export_string = fmt::format("{} {} {}", mp->itemslot, itemid, inst->GetItemType()); + std::vector extra_pointers = { vendor, inst }; + + int result = parse->EventPlayer(EVENT_MERCHANT_PRESELL, this, export_string, 0, &extra_pointers); + // CANCEL: If a script returns -1 for this event, the sale wil be cancelled. Sends a dummy packet sent to satisfy the client + if (result == -1) { + auto outapp = new EQApplicationPacket(OP_ShopPlayerSell, sizeof(Merchant_Purchase_Struct)); + Merchant_Purchase_Struct* mco = (Merchant_Purchase_Struct*)outapp->pBuffer; + mco->npcid = vendor->GetID(); + mco->itemslot = -1; // Critical or the client will remove the item visually + mco->quantity = 0; + mco->price = 0; + QueuePacket(outapp); + safe_delete(outapp); + return; + } } - uint32 cost_quantity = mp->quantity; - if (inst->IsCharged()) - uint32 cost_quantity = 1; - - uint32 i; + uint32 cost_quantity = inst->IsCharged() ? 1 : mp->quantity; + uint32 price = 0; if (RuleB(Merchant, UsePriceMod)) { - for (i = 1; i <= cost_quantity; i++) { + for (uint32 i = 1; i <= cost_quantity; i++) { price = (uint32)(item->Price * i) * Client::CalcPriceMod(vendor, true); // Don't use SellCostMod if using UseClassicPriceMod @@ -14436,7 +14451,7 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) } } else { - for (i = 1; i <= cost_quantity; i++) { + for (uint32 i = 1; i <= cost_quantity; i++) { price = (uint32)((item->Price * i)*(RuleR(Merchant, BuyCostMod)) + 0.5); // need to round up, because client does it automatically when displaying price if (price > 4000000000) { cost_quantity = i; @@ -14448,6 +14463,7 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) AddMoneyToPP(price); + // Update merchant stock and refresh client if (inst->IsStackable() || inst->IsCharged()) { unsigned int i_quan = inst->GetCharges(); @@ -14561,6 +14577,8 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) QueuePacket(outapp); safe_delete(outapp); SendMoneyUpdate(); + + RDTSC_Timer t1(true); t1.start(); Save(1); t1.stop(); @@ -14679,6 +14697,11 @@ void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app) if ((tabs_to_display & Parcel) == Parcel) { SendBulkParcels(); } + + if (parse->PlayerHasQuestSub(EVENT_MERCHANT_OPEN)) { + std::vector extra_pointers = { tmp }; + parse->EventPlayer(EVENT_MERCHANT_OPEN, this, "", 0, &extra_pointers); + } } return; diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 52863b698..c701275b3 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -163,8 +163,10 @@ const char* QuestEventSubroutines[_LargestEventID] = { "EVENT_LANGUAGE_SKILL_UP", "EVENT_ALT_CURRENCY_MERCHANT_BUY", "EVENT_ALT_CURRENCY_MERCHANT_SELL", + "EVENT_MERCHANT_OPEN", "EVENT_MERCHANT_BUY", "EVENT_MERCHANT_SELL", + "EVENT_MERCHANT_PRESELL", "EVENT_INSPECT", "EVENT_TASK_BEFORE_UPDATE", "EVENT_AA_BUY", @@ -2289,6 +2291,33 @@ void PerlembParser::ExportEventVariables( break; } + case EVENT_MERCHANT_OPEN: { + if (!extra_pointers || extra_pointers->size() < 1) break; + + auto mob_ptr = std::any_cast(extra_pointers->at(0)); + if (!mob_ptr) break; + + ExportVar(package_name.c_str(), "other", "Mob", mob_ptr); + break; + } + + case EVENT_MERCHANT_PRESELL: { + Seperator sep(data); + ExportVar(package_name.c_str(), "slot_id", sep.arg[0]); + ExportVar(package_name.c_str(), "item_id", sep.arg[1]); + ExportVar(package_name.c_str(), "item_type", sep.arg[2]); + + if (!extra_pointers || extra_pointers->size() < 2) break; + + auto mob_ptr = std::any_cast(extra_pointers->at(0)); + auto inst_ptr = std::any_cast(extra_pointers->at(1)); + if (!mob_ptr || !inst_ptr) break; + + ExportVar(package_name.c_str(), "other", "Mob", mob_ptr); + ExportVar(package_name.c_str(), "item", "ItemInstance", inst_ptr); + break; + } + case EVENT_AA_BUY: { Seperator sep(data); ExportVar(package_name.c_str(), "aa_cost", sep.arg[0]); diff --git a/zone/event_codes.h b/zone/event_codes.h index c5210633f..fb6c349b1 100644 --- a/zone/event_codes.h +++ b/zone/event_codes.h @@ -116,8 +116,10 @@ enum QuestEventID { EVENT_LANGUAGE_SKILL_UP, EVENT_ALT_CURRENCY_MERCHANT_BUY, EVENT_ALT_CURRENCY_MERCHANT_SELL, + EVENT_MERCHANT_OPEN, EVENT_MERCHANT_BUY, EVENT_MERCHANT_SELL, + EVENT_MERCHANT_PRESELL, EVENT_INSPECT, EVENT_TASK_BEFORE_UPDATE, EVENT_AA_BUY, diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index 793b730c5..dd0402343 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -6968,8 +6968,10 @@ luabind::scope lua_register_events() { luabind::value("language_skill_up", static_cast(EVENT_LANGUAGE_SKILL_UP)), luabind::value("alt_currency_merchant_buy", static_cast(EVENT_ALT_CURRENCY_MERCHANT_BUY)), luabind::value("alt_currency_merchant_sell", static_cast(EVENT_ALT_CURRENCY_MERCHANT_SELL)), + luabind::value("merchant_open", static_cast(EVENT_MERCHANT_OPEN)), luabind::value("merchant_buy", static_cast(EVENT_MERCHANT_BUY)), luabind::value("merchant_sell", static_cast(EVENT_MERCHANT_SELL)), + luabind::value("merchant_presell", static_cast(EVENT_MERCHANT_PRESELL)), luabind::value("inspect", static_cast(EVENT_INSPECT)), luabind::value("task_before_update", static_cast(EVENT_TASK_BEFORE_UPDATE)), luabind::value("aa_buy", static_cast(EVENT_AA_BUY)), diff --git a/zone/lua_iteminst.cpp b/zone/lua_iteminst.cpp index 6e0f97c65..65c3d6d74 100644 --- a/zone/lua_iteminst.cpp +++ b/zone/lua_iteminst.cpp @@ -158,6 +158,11 @@ uint32 Lua_ItemInst::GetItemScriptID() { return self->GetItemScriptID(); } +uint8 Lua_ItemInst::GetItemType() { + Lua_Safe_Call_Int(); + return self->GetItemType(); +} + int Lua_ItemInst::GetCharges() { Lua_Safe_Call_Int(); return self->GetCharges(); @@ -497,6 +502,7 @@ luabind::scope lua_register_iteminst() { .def("GetItemID", (uint32(Lua_ItemInst::*)(int))&Lua_ItemInst::GetItemID) .def("GetItemLink", (std::string(Lua_ItemInst::*)(void))&Lua_ItemInst::GetItemLink) .def("GetItemScriptID", (uint32(Lua_ItemInst::*)(void))&Lua_ItemInst::GetItemScriptID) + .def("GetItemType", (uint8(Lua_ItemInst::*)(void)) & Lua_ItemInst::GetItemType) .def("GetMaxEvolveLvl", (int(Lua_ItemInst::*)(void))&Lua_ItemInst::GetMaxEvolveLvl) .def("GetName", (std::string(Lua_ItemInst::*)(void))&Lua_ItemInst::GetName) .def("GetSerialNumber", (int(Lua_ItemInst::*)(void))&Lua_ItemInst::GetSerialNumber) diff --git a/zone/lua_iteminst.h b/zone/lua_iteminst.h index 49a7e8b82..c0cdf337d 100644 --- a/zone/lua_iteminst.h +++ b/zone/lua_iteminst.h @@ -71,6 +71,7 @@ public: bool IsAmmo(); uint32 GetID(); uint32 GetItemScriptID(); + uint8 GetItemType(); int GetCharges(); void SetCharges(int charges); uint32 GetPrice(); diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index d45ea9506..911ff3779 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -160,8 +160,10 @@ const char *LuaEvents[_LargestEventID] = { "event_language_skill_up", "event_alt_currency_merchant_buy", "event_alt_currency_merchant_sell", + "event_merchant_open", "event_merchant_buy", "event_merchant_sell", + "event_merchant_presell", "event_inspect", "event_task_before_update", "event_aa_buy", @@ -335,8 +337,10 @@ LuaParser::LuaParser() { PlayerArgumentDispatch[EVENT_LANGUAGE_SKILL_UP] = handle_player_language_skill_up; PlayerArgumentDispatch[EVENT_ALT_CURRENCY_MERCHANT_BUY] = handle_player_alt_currency_merchant; PlayerArgumentDispatch[EVENT_ALT_CURRENCY_MERCHANT_SELL] = handle_player_alt_currency_merchant; + PlayerArgumentDispatch[EVENT_MERCHANT_OPEN] = handle_player_merchant_open; PlayerArgumentDispatch[EVENT_MERCHANT_BUY] = handle_player_merchant; PlayerArgumentDispatch[EVENT_MERCHANT_SELL] = handle_player_merchant; + PlayerArgumentDispatch[EVENT_MERCHANT_PRESELL] = handle_player_merchant_presell; PlayerArgumentDispatch[EVENT_INSPECT] = handle_player_inspect; PlayerArgumentDispatch[EVENT_AA_BUY] = handle_player_aa_buy; PlayerArgumentDispatch[EVENT_AA_GAIN] = handle_player_aa_gain; diff --git a/zone/lua_parser_events.cpp b/zone/lua_parser_events.cpp index a768e18c1..12ae30b6b 100644 --- a/zone/lua_parser_events.cpp +++ b/zone/lua_parser_events.cpp @@ -2340,6 +2340,53 @@ void handle_player_merchant( lua_setfield(L, -2, "item_cost"); } +void handle_player_merchant_open( + QuestInterface* parse, + lua_State* L, + Client* client, + std::string data, + uint32 extra_data, + std::vector* extra_pointers +) { + if (!extra_pointers || extra_pointers->size() < 1) return; + + auto mob_ptr = std::any_cast(extra_pointers->at(0)); + if (!mob_ptr) return; + + Lua_Mob l_mob(mob_ptr); + luabind::adl::object l_mob_o = luabind::adl::object(L, l_mob); + l_mob_o.push(L); + lua_setfield(L, -2, "other"); +} + +void handle_player_merchant_presell( + QuestInterface* parse, + lua_State* L, + Client* client, + std::string data, + uint32 extra_data, + std::vector* extra_pointers +) { + Seperator sep(data.c_str()); + lua_pushinteger(L, Strings::ToInt(sep.arg[0])); lua_setfield(L, -2, "slot_id"); + lua_pushinteger(L, Strings::ToInt(sep.arg[1])); lua_setfield(L, -2, "item_id"); + lua_pushinteger(L, Strings::ToInt(sep.arg[2])); lua_setfield(L, -2, "item_type"); + + if (!extra_pointers || extra_pointers->size() < 2) return; + + auto mob_ptr = std::any_cast(extra_pointers->at(0)); + auto inst_ptr = std::any_cast(extra_pointers->at(1)); + if (!mob_ptr || !inst_ptr) return; + + Lua_Mob l_mob(mob_ptr); + luabind::adl::object(L, l_mob).push(L); + lua_setfield(L, -2, "other"); + + Lua_ItemInst l_iteminst(inst_ptr); + luabind::adl::object(L, l_iteminst).push(L); + lua_setfield(L, -2, "item"); +} + void handle_player_augment_insert( QuestInterface *parse, lua_State* L, diff --git a/zone/lua_parser_events.h b/zone/lua_parser_events.h index b436691fd..bb32ad053 100644 --- a/zone/lua_parser_events.h +++ b/zone/lua_parser_events.h @@ -671,6 +671,24 @@ void handle_player_merchant( std::vector *extra_pointers ); +void handle_player_merchant_open( + QuestInterface* parse, + lua_State* L, + Client* client, + std::string data, + uint32 extra_data, + std::vector* extra_pointers +); + +void handle_player_merchant_presell( + QuestInterface* parse, + lua_State* L, + Client* client, + std::string data, + uint32 extra_data, + std::vector* extra_pointers +); + void handle_player_inspect( QuestInterface *parse, lua_State* L,