diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e5cd22c..dcfa1f1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +## [22.60.0] 11/25/2024 + +### Bazaar + +* Further refinements for instanced bazaar ([#4544](https://github.com/EQEmu/Server/pull/4544)) @neckkola 2024-11-16 + +### Code + +* Fix build with older C++ libraries ([#4549](https://github.com/EQEmu/Server/pull/4549)) @hgtw 2024-11-24 + +### Config + +* Fix World TCP Address Configuration Default ([#4551](https://github.com/EQEmu/Server/pull/4551)) @Akkadius 2024-11-24 + +### Fixes + +* Fix Issue with Perl EVENT_PAYLOAD ([#4545](https://github.com/EQEmu/Server/pull/4545)) @Kinglykrab 2024-11-24 +* Fix Possible Item Loss in Trades ([#4554](https://github.com/EQEmu/Server/pull/4554)) @Kinglykrab 2024-11-24 +* Fix Strings::Commify bug with #mystats ([#4547](https://github.com/EQEmu/Server/pull/4547)) @carolus21rex 2024-11-22 +* Fix an edge case with augmented items inside parceled containers ([#4546](https://github.com/EQEmu/Server/pull/4546)) @neckkola 2024-11-21 +* Fix for bazaar search of containers. ([#4540](https://github.com/EQEmu/Server/pull/4540)) @neckkola 2024-11-15 +* Fix for mult-instanced bazaar zones ([#4541](https://github.com/EQEmu/Server/pull/4541)) @neckkola 2024-11-15 +* Fix for sending money via Parcel, then changing your mind ([#4552](https://github.com/EQEmu/Server/pull/4552)) @neckkola 2024-11-24 +* Fix issue where NPC's are being hidden as traders ([#4539](https://github.com/EQEmu/Server/pull/4539)) @Akkadius 2024-11-15 +* Players could become flagged as a Trader when they were not trading ([#4553](https://github.com/EQEmu/Server/pull/4553)) @neckkola 2024-11-24 + +### Rules + +* Add Rule to Disable NPCs Facing Target ([#4543](https://github.com/EQEmu/Server/pull/4543)) @Kinglykrab 2024-11-24 + +### Tasks + +* Update tasks in all zones if invalid zone set ([#4550](https://github.com/EQEmu/Server/pull/4550)) @hgtw 2024-11-25 + ## [22.59.1] 11/13/2024 ### Hotfix diff --git a/common/eqemu_config.cpp b/common/eqemu_config.cpp index 85db27492..40374de8b 100644 --- a/common/eqemu_config.cpp +++ b/common/eqemu_config.cpp @@ -94,7 +94,7 @@ void EQEmuConfig::parse_config() auto_database_updates = true; } - WorldIP = _root["server"]["world"]["tcp"].get("host", "127.0.0.1").asString(); + WorldIP = _root["server"]["world"]["tcp"].get("ip", "127.0.0.1").asString(); WorldTCPPort = Strings::ToUnsignedInt(_root["server"]["world"]["tcp"].get("port", "9000").asString()); TelnetIP = _root["server"]["world"]["telnet"].get("ip", "127.0.0.1").asString(); diff --git a/common/mysql_stmt.cpp b/common/mysql_stmt.cpp index 0c71aa53c..872df0086 100644 --- a/common/mysql_stmt.cpp +++ b/common/mysql_stmt.cpp @@ -414,6 +414,12 @@ static uint64_t MakeBits(std::span data) return bits; } +template +concept has_from_chars = requires (const char* first, const char* last, T value) +{ + std::from_chars(first, last, value); +}; + template static T FromString(std::string_view sv) { @@ -422,6 +428,14 @@ static T FromString(std::string_view sv) // return false for empty (zero-length) strings return !sv.empty(); } + else if constexpr (std::is_same_v && !has_from_chars) + { + return std::strtof(std::string(sv).c_str(), nullptr); + } + else if constexpr (std::is_same_v && !has_from_chars) + { + return std::strtod(std::string(sv).c_str(), nullptr); + } else { // non numbers return a zero initialized T (could return nullopt instead) diff --git a/common/repositories/trader_repository.h b/common/repositories/trader_repository.h index b85d04e76..6c6ab35d2 100644 --- a/common/repositories/trader_repository.h +++ b/common/repositories/trader_repository.h @@ -164,37 +164,35 @@ public: return UpdateOne(db, m); } - static Trader GetItemBySerialNumber(Database &db, uint32 serial_number) + static Trader GetItemBySerialNumber(Database &db, uint32 serial_number, uint32 trader_id) { Trader e{}; const auto trader_item = GetWhere( db, - fmt::format("`item_sn` = '{}' LIMIT 1", serial_number) + fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, serial_number) ); if (trader_item.empty()) { return e; } - else { - return trader_item.at(0); - } + + return trader_item.at(0); } - static Trader GetItemBySerialNumber(Database &db, std::string serial_number) + static Trader GetItemBySerialNumber(Database &db, std::string serial_number, uint32 trader_id) { Trader e{}; auto sn = Strings::ToUnsignedBigInt(serial_number); const auto trader_item = GetWhere( db, - fmt::format("`item_sn` = '{}' LIMIT 1", sn) + fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, sn) ); if (trader_item.empty()) { return e; } - else { - return trader_item.at(0); - } + + return trader_item.at(0); } static int UpdateActiveTransaction(Database &db, uint32 id, bool status) diff --git a/common/ruletypes.h b/common/ruletypes.h index ec6f50dc3..16d8ec2a2 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -682,6 +682,7 @@ RULE_BOOL(NPC, DisableLastNames, false, "Enable to disable NPC Last Names") RULE_BOOL(NPC, NPCIgnoreLevelBasedHasteCaps, false, "Ignores hard coded level based haste caps.") RULE_INT(NPC, NPCHasteCap, 150, "Haste cap for non-v3(over haste) haste") RULE_INT(NPC, NPCHastev3Cap, 25, "Haste cap for v3(over haste) haste") +RULE_STRING(NPC, ExcludedFaceTargetRaces, "52,72,73,141,233,328,329,372,376,377,378,379,380,381,382,383,404,422,423,424,425,426,428,429,445,449,460,462,463,500,501,502,503,504,505,506,507,508,509,510,511,513,514,515,516,533,534,535,536,537,538,539,540,541,542,543,544,545,546,550,551,552,553,554,555,556,557,567,573,577,586,589,590,591,592,593,595,596,599,601,616,619,621,628,629,630,633,634,635,636,665,683,684,685,691,692,693,694,702,703,705,706,707,710,711,714,720,2250,2254", "Race IDs excluded from facing target when hailed") RULE_CATEGORY_END() RULE_CATEGORY(Aggro) diff --git a/common/servertalk.h b/common/servertalk.h index 46a3e08bc..5761c1e79 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -1945,6 +1945,7 @@ struct ServerOP_GuildMessage_Struct { struct TraderMessaging_Struct { uint32 action; uint32 zone_id; + uint32 instance_id; uint32 trader_id; uint32 entity_id; char trader_name[64]; diff --git a/common/tasks.h b/common/tasks.h index 8f081daf5..8cf98335b 100644 --- a/common/tasks.h +++ b/common/tasks.h @@ -83,7 +83,8 @@ struct ActivityInformation { if (zone_ids.empty()) { return true; } - bool found_zone = std::find(zone_ids.begin(), zone_ids.end(), zone_id) != zone_ids.end(); + bool found_zone = std::any_of(zone_ids.begin(), zone_ids.end(), + [zone_id](int id) { return id <= 0 || id == zone_id; }); return found_zone && (zone_version == version || zone_version == -1); } @@ -100,7 +101,7 @@ struct ActivityInformation { out.WriteInt32(activity_type == TaskActivityType::GiveCash ? 1 : goal_count); out.WriteLengthString(skill_list); // used in SkillOn objective type string, "-1" for none out.WriteLengthString(spell_list); // used in CastOn objective type string, "0" for none - out.WriteString(zones); // used in objective zone column and task select "begins in" (may have multiple, "0" for "unknown zone", empty for "ALL") + out.WriteString(zones); // used in ui zone columns and task select "begins in" (may have multiple, invalid id for "Unknown Zone", empty for "ALL") } else { @@ -114,7 +115,7 @@ struct ActivityInformation { out.WriteString(description_override); if (client_version >= EQ::versions::ClientVersion::RoF) { - out.WriteString(zones); // serialized again after description (seems unused) + out.WriteString(zones); // target zone version internal id (unused client side) } } diff --git a/common/version.h b/common/version.h index c615c7c85..a144157a7 100644 --- a/common/version.h +++ b/common/version.h @@ -25,7 +25,7 @@ // Build variables // these get injected during the build pipeline -#define CURRENT_VERSION "22.59.1-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.60.0-dev" // always append -dev to the current version for custom-builds #define LOGIN_VERSION "0.8.0" #define COMPILE_DATE __DATE__ #define COMPILE_TIME __TIME__ diff --git a/package.json b/package.json index f2590babb..26dfe3d29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.59.1", + "version": "22.60.0", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 113374e81..55cf1a4ce 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -1736,7 +1736,7 @@ void PerlembParser::ExportEventVariables( case EVENT_PAYLOAD: { Seperator sep(data); ExportVar(package_name.c_str(), "payload_id", sep.arg[0]); - ExportVar(package_name.c_str(), "payload_value", sep.arg[1]); + ExportVar(package_name.c_str(), "payload_value", sep.argplus[1]); break; } diff --git a/zone/mob.cpp b/zone/mob.cpp index ef544c671..a5d70b607 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -2056,19 +2056,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window) case 0: { mod2a_name = "Avoidance"; mod2b_name = "Combat Effects"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemAvoidanceCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemCombatEffectsCap)); + mod2a_cap = RuleI(Character, ItemAvoidanceCap); + mod2b_cap = RuleI(Character, ItemCombatEffectsCap); if (IsBot()) { - mod2a = Strings::Commify(CastToBot()->GetAvoidance()); + mod2a = CastToBot()->GetAvoidance(); } else if (IsClient()) { - mod2a = Strings::Commify(CastToClient()->GetAvoidance()); + mod2a = CastToClient()->GetAvoidance(); } if (IsBot()) { - mod2b = Strings::Commify(CastToBot()->GetCombatEffects()); + mod2b = CastToBot()->GetCombatEffects(); } else if (IsClient()) { - mod2b = Strings::Commify(CastToClient()->GetCombatEffects()); + mod2b = CastToClient()->GetCombatEffects(); } break; @@ -2076,19 +2076,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window) case 1: { mod2a_name = "Accuracy"; mod2b_name = "Strikethrough"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemAccuracyCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemStrikethroughCap)); + mod2a_cap = RuleI(Character, ItemAccuracyCap); + mod2b_cap = RuleI(Character, ItemStrikethroughCap); if (IsBot()) { - mod2a = Strings::Commify(CastToBot()->GetAccuracy()); + mod2a = CastToBot()->GetAccuracy(); } else if (IsClient()) { - mod2a = Strings::Commify(CastToClient()->GetAccuracy()); + mod2a = CastToClient()->GetAccuracy(); } if (IsBot()) { - mod2b = Strings::Commify(CastToBot()->GetStrikeThrough()); + mod2b = CastToBot()->GetStrikeThrough(); } else if (IsClient()) { - mod2b = Strings::Commify(CastToClient()->GetStrikeThrough()); + mod2b = CastToClient()->GetStrikeThrough(); } break; @@ -2096,20 +2096,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window) case 2: { mod2a_name = "Shielding"; mod2b_name = "Spell Shielding"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemShieldingCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemSpellShieldingCap)); + mod2a_cap = RuleI(Character, ItemShieldingCap); + mod2b_cap = RuleI(Character, ItemSpellShieldingCap); if (IsBot()) { - mod2a = Strings::Commify(CastToBot()->GetShielding()); + mod2a = CastToBot()->GetShielding(); } else if (IsClient()) { - mod2a = Strings::Commify(CastToClient()->GetShielding()); + mod2a = CastToClient()->GetShielding(); } if (IsBot()) { - mod2b = Strings::Commify(CastToBot()->GetSpellShield()); + mod2b = CastToBot()->GetSpellShield(); } else if (IsClient()) { - mod2b = Strings::Commify(CastToClient()->GetSpellShield()); + mod2b = CastToClient()->GetSpellShield(); } break; @@ -2117,19 +2117,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window) case 3: { mod2a_name = "Stun Resist"; mod2b_name = "DOT Shielding"; - mod2a_cap = Strings::Commify(RuleI(Character, ItemStunResistCap)); - mod2b_cap = Strings::Commify(RuleI(Character, ItemDoTShieldingCap)); + mod2a_cap = RuleI(Character, ItemStunResistCap); + mod2b_cap = RuleI(Character, ItemDoTShieldingCap); if (IsBot()) { - mod2a = Strings::Commify(CastToBot()->GetStunResist()); + mod2a = CastToBot()->GetStunResist(); } else if (IsClient()) { - mod2a = Strings::Commify(CastToClient()->GetStunResist()); + mod2a = CastToClient()->GetStunResist(); } if (IsBot()) { - mod2b = Strings::Commify(CastToBot()->GetDoTShield()); + mod2b = CastToBot()->GetDoTShield(); } else if (IsClient()) { - mod2b = Strings::Commify(CastToClient()->GetDoTShield()); + mod2b = CastToClient()->GetDoTShield(); } break; diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index fa2a48aa5..69255073b 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -1815,12 +1815,18 @@ void Mob::AI_Event_NoLongerEngaged() { StopNavigation(); ClearRampage(); - parse->EventBotMercNPC(EVENT_COMBAT, this, nullptr, [&]() { return "0"; }); - if (IsNPC()) { SetPrimaryAggro(false); SetAssistAggro(false); - if (CastToNPC()->GetCombatEvent() && GetHP() > 0) { + if ( + CastToNPC()->GetCombatEvent() && + GetHP() > 0 && + entity_list.GetNPCByID(GetID()) + ) { + if (parse->HasQuestSub(GetNPCTypeID(), EVENT_COMBAT)) { + parse->EventNPC(EVENT_COMBAT, CastToNPC(), nullptr, "0", 0); + } + const uint32 emote_id = CastToNPC()->GetEmoteID(); if (emote_id) { CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::LeaveCombat, emote_id); @@ -1829,6 +1835,8 @@ void Mob::AI_Event_NoLongerEngaged() { m_combat_record.Stop(); CastToNPC()->SetCombatEvent(false); } + } else { + parse->EventBotMerc(EVENT_COMBAT, this, nullptr, [&]() { return "0"; }); } } diff --git a/zone/npc.cpp b/zone/npc.cpp index f1260cd9c..38f597e63 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -3297,16 +3297,28 @@ uint32 NPC::GetSpawnKillCount() return(0); } -void NPC::DoQuestPause(Mob *other) { - if(IsMoving() && !IsOnHatelist(other)) { - PauseWandering(RuleI(NPC, SayPauseTimeInSec)); - if (other && !other->sneaking) - FaceTarget(other); - } else if(!IsMoving()) { - if (other && !other->sneaking && GetAppearance() != eaSitting && GetAppearance() != eaDead) - FaceTarget(other); +void NPC::DoQuestPause(Mob* m) +{ + if (!m) { + return; } + if (IsMoving() && !IsOnHatelist(m)) { + PauseWandering(RuleI(NPC, SayPauseTimeInSec)); + + if (FacesTarget() && !m->sneaking) { + FaceTarget(m); + } + } else if (!IsMoving()) { + if ( + FacesTarget() && + !m->sneaking && + GetAppearance() != eaSitting && + GetAppearance() != eaDead + ) { + FaceTarget(m); + } + } } void NPC::ChangeLastName(std::string last_name) @@ -4238,3 +4250,17 @@ void NPC::DoNpcToNpcAggroScan() false ); } + +bool NPC::FacesTarget() +{ + const std::string& excluded_races_rule = RuleS(NPC, ExcludedFaceTargetRaces); + + if (excluded_races_rule.empty()) { + return true; + } + + const auto& v = Strings::Split(excluded_races_rule, ","); + + return std::find(v.begin(), v.end(), std::to_string(GetBaseRace())) == v.end(); +} + diff --git a/zone/npc.h b/zone/npc.h index 20b6f74f6..a20d975b1 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -482,7 +482,8 @@ public: NPC_Emote_Struct* GetNPCEmote(uint32 emote_id, uint8 event_); void DoNPCEmote(uint8 event_, uint32 emote_id, Mob* t = nullptr); bool CanTalk(); - void DoQuestPause(Mob *other); + void DoQuestPause(Mob* m); + bool FacesTarget(); inline void SetSpellScale(float amt) { spellscale = amt; } inline float GetSpellScale() { return spellscale; } diff --git a/zone/parcels.cpp b/zone/parcels.cpp index 259e495ea..e3661906f 100644 --- a/zone/parcels.cpp +++ b/zone/parcels.cpp @@ -278,6 +278,19 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) return; } + if (parcel_in->money_flag && parcel_in->item_slot != INVALID_INDEX) { + Message( + Chat::Yellow, + fmt::format( + "{} tells you, 'I am confused! Do you want to send money or an item?'", + merchant->GetCleanName() + ).c_str() + ); + DoParcelCancel(); + SendParcelAck(); + return; + } + auto num_of_parcels = GetParcelCount(); if (num_of_parcels >= RuleI(Parcel, ParcelMaxItems)) { SendParcelIconStatus(); @@ -406,9 +419,8 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in) std::vector all_entries{}; if (inst->IsNoneEmptyContainer()) { - CharacterParcelsContainersRepository::CharacterParcelsContainers cpc{}; - for (auto const &kv: *inst->GetContents()) { + CharacterParcelsContainersRepository::CharacterParcelsContainers cpc{}; cpc.parcels_id = result.id; cpc.slot_id = kv.first; cpc.item_id = kv.second->GetID(); diff --git a/zone/trading.cpp b/zone/trading.cpp index 7a8abc2e9..f963f7ca1 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -777,6 +777,8 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st tradingWith->SayString(TRADE_BACK, GetCleanName()); PushItemOnCursor(*inst, true); } + + items.clear(); } // Only enforce trade rules if the NPC doesn't have an EVENT_TRADE // subroutine. That overrides all. @@ -2913,10 +2915,11 @@ void Client::SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions a auto outapp = new ServerPacket(ServerOP_TraderMessaging, sizeof(TraderMessaging_Struct)); auto data = (TraderMessaging_Struct *) outapp->pBuffer; - data->action = action; - data->entity_id = trader->GetID(); - data->trader_id = trader->CharacterID(); - data->zone_id = trader->GetZoneID(); + data->action = action; + data->entity_id = trader->GetID(); + data->trader_id = trader->CharacterID(); + data->zone_id = trader->GetZoneID(); + data->instance_id = trader->GetInstanceID(); strn0cpy(data->trader_name, trader->GetName(), sizeof(data->trader_name)); worldserver.SendPacket(outapp); @@ -3235,7 +3238,10 @@ void Client::SendBulkBazaarTraders() void Client::DoBazaarInspect(const BazaarInspect_Struct &in) { - auto items = TraderRepository::GetWhere(database, fmt::format("item_sn = {}", in.serial_number)); + auto items = TraderRepository::GetWhere( + database, fmt::format("`char_id` = '{}' AND `item_sn` = '{}'", in.trader_id, in.serial_number) + ); + if (items.empty()) { LogInfo("Failed to find item with serial number [{}]", in.serial_number); return; @@ -3304,7 +3310,7 @@ std::string Client::DetermineMoneyString(uint64 cp) void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicationPacket *app) { auto in = (TraderBuy_Struct *) app->pBuffer; - auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number); + auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number, tbs->trader_id); if (!trader_item.id) { LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " "[{}] The Traders data was outdated.", diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index acfb686df..afff2a534 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -3942,7 +3942,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) c.second->QueuePacket(outapp); safe_delete(outapp); } - if (zone && zone->GetZoneID() == Zones::BAZAAR) { + if (zone && zone->GetZoneID() == Zones::BAZAAR && in->instance_id == zone->GetInstanceID()) { if (in->action == TraderOn) { c.second->SendBecomeTrader(TraderOn, in->entity_id); }