diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9db8b1ac5..2314fe365 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -88,7 +88,7 @@ jobs: - name: Configure shell: pwsh run: | - cmake -S . -B build -G "Visual Studio 17 2022" -A x64 ` + cmake -S . -B build -G "Visual Studio 18 2026" -A x64 ` -DCMAKE_BUILD_TYPE=Release ` -DEQEMU_BUILD_TESTS=ON ` -DEQEMU_BUILD_LOGIN=ON ` diff --git a/zone/bot.cpp b/zone/bot.cpp index 280904315..11877f4b9 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -5335,6 +5335,12 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { if (ma_time) { switch (GetClass()) { case Class::Monk: { + + if (!GetSkill(EQ::skills::SkillTigerClaw)) { + monkattack_timer.Disable(); + return; + } + int reuse = (MonkSpecialAttack(target, EQ::skills::SkillTigerClaw) - 1); // Live AA - Technique of Master Wu diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index e583496e6..cf03ff176 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -512,7 +512,7 @@ bool BotDatabase::SaveNewBot(Bot* b, uint32& bot_id) e.poison = b->GetBasePR(); e.disease = b->GetBaseDR(); e.corruption = b->GetBaseCorrup(); - e.expansion_bitmask = b->GetExpansionBitmask(); + e.expansion_bitmask = RuleI(Bots, BotExpansionSettings); e = BotDataRepository::InsertOne(database, e); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index cdaf808de..783fc1c1b 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -14361,9 +14361,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) @@ -14373,35 +14372,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 @@ -14419,7 +14434,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; @@ -14431,6 +14446,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(); @@ -14544,6 +14560,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(); @@ -14665,6 +14683,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 d0f41d94e..7ed846311 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 cf40f4464..0a8ec5c95 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 a1c26c9d1..8647f0538 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 be590a007..d44d8daab 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 be1fd9967..ae7f103e2 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, diff --git a/zone/main.cpp b/zone/main.cpp index 9aab5adf8..e3ebb1959 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -711,48 +711,36 @@ void UpdateWindowTitle(char *iNewTitle) bool CheckForCompatibleQuestPlugins() { - const std::vector> directories = { - {"lua_modules", nullptr}, - {"plugins", nullptr} - }; - bool lua_found = false; bool perl_found = false; - try { - for (const auto &[directory, flag]: directories) { - std::string dir_path = PathManager::Instance()->GetServerPath() + "/" + directory; - if (!File::Exists(dir_path)) { continue; } - - for (const auto &file: fs::directory_iterator(dir_path)) { + auto check_dir = [&](const std::string& dir_path, bool& found) { + if (!File::Exists(dir_path)) { return; } + try { + for (const auto& file : fs::directory_iterator(dir_path)) { if (!file.is_regular_file()) { continue; } - - std::string file_path = file.path().string(); - if (!File::Exists(file_path)) { continue; } - - auto r = File::GetContents(file_path); - if (!Strings::Contains(r.contents, "CheckHandin")) { continue; } - - if (directory == "lua_modules") { - lua_found = true; + auto r = File::GetContents(file.path().string()); + if (Strings::Contains(r.contents, "CheckHandin")) { + found = true; + return; } - else { - perl_found = true; - } - - if (lua_found && perl_found) { return true; } } } - } catch (const fs::filesystem_error &ex) { - LogError("Failed to check for compatible quest plugins: {}", ex.what()); + catch (const fs::filesystem_error& ex) { + LogError("Failed to check for compatible quest plugins: {}", ex.what()); + } + }; + + for (const auto& path : PathManager::Instance()->GetLuaModulePaths()) { + check_dir(path, lua_found); } - if (!lua_found) { - LogError("Failed to find CheckHandin in lua_modules"); - } - if (!perl_found) { - LogError("Failed to find CheckHandin in plugins"); + for (const auto& path : PathManager::Instance()->GetPluginPaths()) { + check_dir(path, perl_found); } + if (!lua_found) { LogError("Failed to find CheckHandin in the Lua module quest directories"); } + if (!perl_found) { LogError("Failed to find CheckHandin in the Perl plugins quest directories");} + return lua_found && perl_found; } diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index 466ab7d58..0b3e30e65 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -512,29 +512,6 @@ void Client::OPCombatAbility(const CombatAbility_Struct *ca_atk) bool found_skill = false; - if ( - ca_atk->m_atk == 100 && - ca_atk->m_skill == EQ::skills::SkillKick && - can_use_kick - ) { - if (GetTarget() != this) { - CheckIncreaseSkill(EQ::skills::SkillKick, GetTarget(), 10); - DoAnim(animKick, 0, false); - - int hate_override = 0; - if (GetWeaponDamage(GetTarget(), GetInv().GetItem(EQ::invslot::slotFeet)) <= 0) { - damage = -5; - } else { - hate_override = damage = GetBaseSkillDamage(EQ::skills::SkillKick, GetTarget()); - } - - reuse_time = KickReuseTime - 1 - skill_reduction; - DoSpecialAttackDamage(GetTarget(), EQ::skills::SkillKick, damage, 0, hate_override, reuse_time); - - found_skill = true; - } - } - if (class_id == Class::Monk) { reuse_time = MonkSpecialAttack(GetTarget(), ca_atk->m_skill) - 1 - skill_reduction; @@ -596,6 +573,30 @@ void Client::OPCombatAbility(const CombatAbility_Struct *ca_atk) found_skill = true; } + else { + if ( + ca_atk->m_atk == 100 && + ca_atk->m_skill == EQ::skills::SkillKick && + can_use_kick + ) { + if (GetTarget() != this) { + CheckIncreaseSkill(EQ::skills::SkillKick, GetTarget(), 10); + DoAnim(animKick, 0, false); + + int hate_override = 0; + if (GetWeaponDamage(GetTarget(), GetInv().GetItem(EQ::invslot::slotFeet)) <= 0) { + damage = -5; + } else { + hate_override = damage = GetBaseSkillDamage(EQ::skills::SkillKick, GetTarget()); + } + + reuse_time = KickReuseTime - 1 - skill_reduction; + DoSpecialAttackDamage(GetTarget(), EQ::skills::SkillKick, damage, 0, hate_override, reuse_time); + + found_skill = true; + } + } + } if ( ca_atk->m_atk == 100 &&