From 311af7bbe9c0203711ccd53f389ac1ba00807e13 Mon Sep 17 00:00:00 2001 From: carolus21rex <85852042+carolus21rex@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:59:10 -0400 Subject: [PATCH 01/26] [Cleanup] Fixed a typo in Zoning.cpp (#4515) * Fixed a typo in Zoning.cpp changed reguest to request. * Update zoning.cpp --------- Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com> --- zone/zoning.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/zoning.cpp b/zone/zoning.cpp index 7b3f70962..225d21a91 100644 --- a/zone/zoning.cpp +++ b/zone/zoning.cpp @@ -737,7 +737,7 @@ void Client::ProcessMovePC(uint32 zoneID, uint32 instance_id, float x, float y, ZonePC(zoneID, instance_id, x, y, z, heading, ignorerestrictions, zm); break; default: - LogError("Client::ProcessMovePC received a reguest to perform an unsupported client zone operation"); + LogError("Received a request to perform an unsupported client zone operation"); break; } } From cf3483b4027aa0371a7da1a83b76b799c14b5d57 Mon Sep 17 00:00:00 2001 From: nytmyr <53322305+nytmyr@users.noreply.github.com> Date: Sun, 20 Oct 2024 09:44:30 -0500 Subject: [PATCH 02/26] [Bots] Fix timers loading on spawn and zone (#4516) Timers were not properly checking their expiration time on spawn and load and could cause invalid timers to load if the server was restarted resulting in improper lockouts. --- zone/bot_database.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index 3a07b2649..dcc5ff89b 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -828,7 +828,7 @@ bool BotDatabase::LoadTimers(Bot* b) BotTimer_Struct t{ }; for (const auto& e : l) { - if (t.timer_value < (Timer::GetCurrentTime() + t.recast_time)) { + if (e.timer_value < (Timer::GetCurrentTime() + e.recast_time)) { t.timer_id = e.timer_id; t.timer_value = e.timer_value; t.recast_time = e.recast_time; From 9583099ace0002f8f2d12505ee2fd764db00492b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 20 Oct 2024 16:17:15 -0500 Subject: [PATCH 03/26] [Release] 22.57.0 (#4517) --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ common/version.h | 2 +- package.json | 2 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7cc690a..71e30cf6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## [22.57.0] 10/20/2024 + +### Bots + +* Add "silent" option to ^spawn and mute raid spawn ([#4494](https://github.com/EQEmu/Server/pull/4494)) @nytmyr 2024-10-05 +* Add attack flag when told to attack ([#4490](https://github.com/EQEmu/Server/pull/4490)) @nytmyr 2024-09-29 +* Fix timers loading on spawn and zone ([#4516](https://github.com/EQEmu/Server/pull/4516)) @nytmyr 2024-10-20 + +### Code + +* Fixed a typo in Zoning.cpp ([#4515](https://github.com/EQEmu/Server/pull/4515)) @carolus21rex 2024-10-20 +* Optimization Code Cleanup ([#4489](https://github.com/EQEmu/Server/pull/4489)) @Akkadius 2024-09-30 +* Remove Extra Skill in EQ::skills::GetExtraDamageSkills() ([#4486](https://github.com/EQEmu/Server/pull/4486)) @Kinglykrab 2024-10-03 + +### Crash + +* Fixes a crash when the faction_list db table is empty. ([#4511](https://github.com/EQEmu/Server/pull/4511)) @KimLS 2024-10-14 + +### Fixes + +* Add character_instance_safereturns to tables_to_zero_id ([#4485](https://github.com/EQEmu/Server/pull/4485)) @Morzain 2024-09-26 +* Correctly limit max targets of PBAOE ([#4507](https://github.com/EQEmu/Server/pull/4507)) @catapultam-habeo 2024-10-11 +* FindBestZ selecting false zone floor as bestz - Results in roambox failures ([#4504](https://github.com/EQEmu/Server/pull/4504)) @fryguy503 2024-10-13 +* Fix #set motd Crash ([#4495](https://github.com/EQEmu/Server/pull/4495)) @Kinglykrab 2024-10-05 +* Fix `character_exp_modifiers` Default Values ([#4502](https://github.com/EQEmu/Server/pull/4502)) @Kinglykrab 2024-10-09 +* Fix a display error regarding a few trader/buyer query errors ([#4514](https://github.com/EQEmu/Server/pull/4514)) @neckkola 2024-10-17 +* Fix Group ID 0 in Group::SaveGroupLeaderAA() ([#4487](https://github.com/EQEmu/Server/pull/4487)) @Kinglykrab 2024-10-03 +* Fix Mercenary Encounter Crash ([#4509](https://github.com/EQEmu/Server/pull/4509)) @Kinglykrab 2024-10-12 +* Fix NPC::CanTalk() Crash ([#4499](https://github.com/EQEmu/Server/pull/4499)) @Kinglykrab 2024-10-07 +* Fix Spells:DefaultAOEMaxTargets Default Value ([#4508](https://github.com/EQEmu/Server/pull/4508)) @Kinglykrab 2024-10-12 +* Fix Targeted AOE Max Targets Rule ([#4488](https://github.com/EQEmu/Server/pull/4488)) @Kinglykrab 2024-10-03 +* fixed a bug where it would use npc value instead of faction value in the database. ([#4491](https://github.com/EQEmu/Server/pull/4491)) @regneq 2024-09-29 +* Master of Disguise should apply to illusions casted by others. ([#4506](https://github.com/EQEmu/Server/pull/4506)) @fryguy503 2024-10-11 +* Spells - Self Only (Yellow) cast when non group member is targeted ([#4503](https://github.com/EQEmu/Server/pull/4503)) @fryguy503 2024-10-11 + +### Loginserver + +* Larion loginserver support ([#4492](https://github.com/EQEmu/Server/pull/4492)) @KimLS 2024-10-03 +* Login Fatal Error Spamming ([#4476](https://github.com/EQEmu/Server/pull/4476)) @KimLS 2024-10-09 + +### Logs + +* Add NPC Trades to Player Events ([#4505](https://github.com/EQEmu/Server/pull/4505)) @Kinglykrab 2024-10-13 + +### Quest API + +* Add Buff Fade Methods to Perl/Lua ([#4501](https://github.com/EQEmu/Server/pull/4501)) @Kinglykrab 2024-10-09 +* Add EVENT_READ_ITEM to Perl/Lua ([#4497](https://github.com/EQEmu/Server/pull/4497)) @Kinglykrab 2024-10-08 +* Add NPC List Filter Methods to Perl/Lua ([#4493](https://github.com/EQEmu/Server/pull/4493)) @Kinglykrab 2024-10-04 +* Add Scripting Support to Mercenaries ([#4500](https://github.com/EQEmu/Server/pull/4500)) @Kinglykrab 2024-10-11 + +### Rules + +* Add Rule to disable PVP Regions ([#4513](https://github.com/EQEmu/Server/pull/4513)) @Kinglykrab 2024-10-17 + ## [22.56.3] 9/23/2024 ### Fixes diff --git a/common/version.h b/common/version.h index 91d4c7494..6e1c31abc 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.56.3-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.57.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 c73e7c47a..42a331bc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.56.3", + "version": "22.57.0", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" From e6469878ced7608e72880218be0f7b3faa0a85c8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Oct 2024 21:48:43 -0700 Subject: [PATCH 04/26] [Loginserver] Automatifc Opcode File Creation (#4521) * Loginserver will auto create the opcodes file if it doesn't exist on load. * Use path manager in login opcodes. --------- Co-authored-by: KimLS --- common/eqemu_config.cpp | 1 + common/eqemu_config.h | 1 + common/path_manager.cpp | 11 ++++ common/path_manager.h | 2 + loginserver/client_manager.cpp | 103 +++++++++++++++++++++++++++------ 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/common/eqemu_config.cpp b/common/eqemu_config.cpp index 56b8ff989..85db27492 100644 --- a/common/eqemu_config.cpp +++ b/common/eqemu_config.cpp @@ -171,6 +171,7 @@ void EQEmuConfig::parse_config() PluginDir = _root["server"]["directories"].get("plugins", "plugins/").asString(); LuaModuleDir = _root["server"]["directories"].get("lua_modules", "lua_modules/").asString(); PatchDir = _root["server"]["directories"].get("patches", "./").asString(); + OpcodeDir = _root["server"]["directories"].get("opcodes", "./").asString(); SharedMemDir = _root["server"]["directories"].get("shared_memory", "shared/").asString(); LogDir = _root["server"]["directories"].get("logs", "logs/").asString(); diff --git a/common/eqemu_config.h b/common/eqemu_config.h index 6aad3d41c..7e9e3b341 100644 --- a/common/eqemu_config.h +++ b/common/eqemu_config.h @@ -95,6 +95,7 @@ class EQEmuConfig std::string PluginDir; std::string LuaModuleDir; std::string PatchDir; + std::string OpcodeDir; std::string SharedMemDir; std::string LogDir; diff --git a/common/path_manager.cpp b/common/path_manager.cpp index fba5f777a..452722c84 100644 --- a/common/path_manager.cpp +++ b/common/path_manager.cpp @@ -74,6 +74,11 @@ void PathManager::LoadPaths() m_patch_path = fs::relative(fs::path{m_server_path + "/" + c->PatchDir}).string(); } + // patches + if (File::Exists(fs::path{ m_server_path + "/" + c->OpcodeDir }.string())) { + m_opcode_path = fs::relative(fs::path{ m_server_path + "/" + c->OpcodeDir }).string(); + } + // shared_memory_path if (File::Exists(fs::path{m_server_path + "/" + c->SharedMemDir}.string())) { m_shared_memory_path = fs::relative(fs::path{ m_server_path + "/" + c->SharedMemDir }).string(); @@ -89,6 +94,7 @@ void PathManager::LoadPaths() LogInfo("lua_modules path [{}]", m_lua_modules_path); LogInfo("maps path [{}]", m_maps_path); LogInfo("patches path [{}]", m_patch_path); + LogInfo("opcode path [{}]", m_opcode_path); LogInfo("plugins path [{}]", m_plugins_path); LogInfo("quests path [{}]", m_quests_path); LogInfo("shared_memory path [{}]", m_shared_memory_path); @@ -129,6 +135,11 @@ const std::string &PathManager::GetPatchPath() const return m_patch_path; } +const std::string &PathManager::GetOpcodePath() const +{ + return m_opcode_path; +} + const std::string &PathManager::GetLuaModulesPath() const { return m_lua_modules_path; diff --git a/common/path_manager.h b/common/path_manager.h index 4283dbf64..3ef55d0a7 100644 --- a/common/path_manager.h +++ b/common/path_manager.h @@ -13,6 +13,7 @@ public: [[nodiscard]] const std::string &GetLuaModulesPath() const; [[nodiscard]] const std::string &GetMapsPath() const; [[nodiscard]] const std::string &GetPatchPath() const; + [[nodiscard]] const std::string &GetOpcodePath() const; [[nodiscard]] const std::string &GetPluginsPath() const; [[nodiscard]] const std::string &GetQuestsPath() const; [[nodiscard]] const std::string &GetServerPath() const; @@ -24,6 +25,7 @@ private: std::string m_lua_modules_path; std::string m_maps_path; std::string m_patch_path; + std::string m_opcode_path; std::string m_plugins_path; std::string m_quests_path; std::string m_server_path; diff --git a/loginserver/client_manager.cpp b/loginserver/client_manager.cpp index 8567c35b4..c82eb0de8 100644 --- a/loginserver/client_manager.cpp +++ b/loginserver/client_manager.cpp @@ -7,6 +7,79 @@ extern bool run_server; #include "../common/eqemu_logsys.h" #include "../common/misc.h" #include "../common/path_manager.h" +#include "../common/file.h" + +void CheckTitaniumOpcodeFile(const std::string &path) { + if (File::Exists(path)) { + return; + } + + auto f = fopen(path.c_str(), "w"); + if (f) { + fprintf(f, "#EQEmu Public Login Server OPCodes\n"); + fprintf(f, "OP_SessionReady=0x0001\n"); + fprintf(f, "OP_Login=0x0002\n"); + fprintf(f, "OP_ServerListRequest=0x0004\n"); + fprintf(f, "OP_PlayEverquestRequest=0x000d\n"); + fprintf(f, "OP_PlayEverquestResponse=0x0021\n"); + fprintf(f, "OP_ChatMessage=0x0016\n"); + fprintf(f, "OP_LoginAccepted=0x0017\n"); + fprintf(f, "OP_ServerListResponse=0x0018\n"); + fprintf(f, "OP_Poll=0x0029\n"); + fprintf(f, "OP_EnterChat=0x000f\n"); + fprintf(f, "OP_PollResponse=0x0011\n"); + fclose(f); + } +} + +void CheckSoDOpcodeFile(const std::string& path) { + if (File::Exists(path)) { + return; + } + + auto f = fopen(path.c_str(), "w"); + if (f) { + fprintf(f, "#EQEmu Public Login Server OPCodes\n"); + fprintf(f, "OP_SessionReady=0x0001\n"); + fprintf(f, "OP_Login=0x0002\n"); + fprintf(f, "OP_ServerListRequest=0x0004\n"); + fprintf(f, "OP_PlayEverquestRequest=0x000d\n"); + fprintf(f, "OP_PlayEverquestResponse=0x0022\n"); + fprintf(f, "OP_ChatMessage=0x0017\n"); + fprintf(f, "OP_LoginAccepted=0x0018\n"); + fprintf(f, "OP_ServerListResponse=0x0019\n"); + fprintf(f, "OP_Poll=0x0029\n"); + fprintf(f, "OP_LoginExpansionPacketData=0x0031\n"); + fprintf(f, "OP_EnterChat=0x000f\n"); + fprintf(f, "OP_PollResponse=0x0011\n"); + fclose(f); + } +} + +void CheckLarionOpcodeFile(const std::string& path) { + if (File::Exists(path)) { + return; + } + + auto f = fopen(path.c_str(), "w"); + if (f) { + fprintf(f, "#EQEmu Public Login Server OPCodes\n"); + fprintf(f, "OP_SessionReady=0x0001\n"); + fprintf(f, "OP_Login=0x0002\n"); + fprintf(f, "OP_ServerListRequest=0x0004\n"); + fprintf(f, "OP_PlayEverquestRequest=0x000d\n"); + fprintf(f, "OP_PlayEverquestResponse=0x0022\n"); + fprintf(f, "OP_ChatMessage=0x0017\n"); + fprintf(f, "OP_LoginAccepted=0x0018\n"); + fprintf(f, "OP_ServerListResponse=0x0019\n"); + fprintf(f, "OP_Poll=0x0029\n"); + fprintf(f, "OP_EnterChat=0x000f\n"); + fprintf(f, "OP_PollResponse=0x0011\n"); + fprintf(f, "OP_SystemFingerprint=0x0016\n"); + fprintf(f, "OP_ExpansionList=0x0030\n"); + fclose(f); + } +} ClientManager::ClientManager() { @@ -19,14 +92,12 @@ ClientManager::ClientManager() std::string opcodes_path = fmt::format( "{}/{}", - path.GetServerPath(), - server.config.GetVariableString( - "client_configuration", - "titanium_opcodes", - "login_opcodes.conf" - ) + path.GetOpcodePath(), + "login_opcodes.conf" ); + CheckTitaniumOpcodeFile(opcodes_path); + if (!titanium_ops->LoadOpcodes(opcodes_path.c_str())) { LogError( "ClientManager fatal error: couldn't load opcodes for Titanium file [{0}]", @@ -58,14 +129,12 @@ ClientManager::ClientManager() opcodes_path = fmt::format( "{}/{}", - path.GetServerPath(), - server.config.GetVariableString( - "client_configuration", - "sod_opcodes", - "login_opcodes.conf" - ) + path.GetOpcodePath(), + "login_opcodes_sod.conf" ); + CheckSoDOpcodeFile(opcodes_path); + if (!sod_ops->LoadOpcodes(opcodes_path.c_str())) { LogError( "ClientManager fatal error: couldn't load opcodes for SoD file {0}", @@ -98,14 +167,12 @@ ClientManager::ClientManager() opcodes_path = fmt::format( "{}/{}", - path.GetServerPath(), - server.config.GetVariableString( - "client_configuration", - "larion_opcodes", - "login_opcodes.conf" - ) + path.GetOpcodePath(), + "login_opcodes_larion.conf" ); + CheckLarionOpcodeFile(opcodes_path); + if (!larion_ops->LoadOpcodes(opcodes_path.c_str())) { LogError( "ClientManager fatal error: couldn't load opcodes for Larion file [{0}]", From d524cb6a5a44387e5989724f4111bb6794d7fd87 Mon Sep 17 00:00:00 2001 From: Alex King <89047260+Kinglykrab@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:49:36 -0400 Subject: [PATCH 05/26] [Bots] Enable Bot Commands Only if Rule Enabled (#4519) --- zone/command.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zone/command.cpp b/zone/command.cpp index b701f2bd2..513cd0218 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -40,7 +40,7 @@ extern FastMath g_Math; void CatchSignal(int sig_num); -int command_count; // how many commands we have +int command_count; // how many commands we have // this is the pointer to the dispatch function, updated once // init has been performed to point at the real function @@ -96,7 +96,7 @@ int command_init(void) command_add("augmentitem", "Force augments an item. Must have the augment item window open.", AccountStatus::GMImpossible, command_augmentitem) || command_add("ban", "[Character Name] [Reason] - Ban by character name", AccountStatus::GMLeadAdmin, command_ban) || command_add("bugs", "[Close|Delete|Review|Search|View] - Handles player bug reports", AccountStatus::QuestTroupe, command_bugs) || - command_add("bot", "Type \"#bot help\" or \"^help\" to the see the list of available commands for bots.", AccountStatus::Player, command_bot) || + (RuleB(Bots, Enabled) && command_add("bot", "Type \"#bot help\" or \"^help\" to the see the list of available commands for bots.", AccountStatus::Player, command_bot)) || command_add("camerashake", "[Duration (Milliseconds)] [Intensity (1-10)] - Shakes the camera on everyone's screen globally.", AccountStatus::QuestTroupe, command_camerashake) || command_add("castspell", "[Spell ID] [Instant (0 = False, 1 = True, Default is 1 if Unused)] - Cast a spell", AccountStatus::Guide, command_castspell) || command_add("chat", "[Channel ID] [Message] - Send a channel message to all zones", AccountStatus::GMMgmt, command_chat) || From d6d5d992cb685ff27f2a51d858179ee1dca45668 Mon Sep 17 00:00:00 2001 From: nytmyr <53322305+nytmyr@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:57:42 -0500 Subject: [PATCH 06/26] [Bots] Fix pet buffs from saving duplicates every save (#4520) * [Bots] Fix pet buffs from saving duplicates every save Previously we were not checking the pet index properly when clearing buffs in the database before saving which resulted in no prior data being deleted. This corrects the logic for the save and also will clean up any buffs for pets that don't exist in the table. * Changes * Update world_boot.cpp --------- Co-authored-by: Akkadius --- world/world_boot.cpp | 7 +++++++ zone/bot_database.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/world/world_boot.cpp b/world/world_boot.cpp index 859eff534..831babdb7 100644 --- a/world/world_boot.cpp +++ b/world/world_boot.cpp @@ -294,6 +294,13 @@ bool WorldBoot::DatabaseLoadRoutines(int argc, char **argv) database.ClearBuyerDetails(); LogInfo("Clearing buyer table details"); + if (RuleB(Bots, Enabled)) { + LogInfo("Clearing [bot_pet_buffs] table of stale entries"); + database.QueryDatabase( + "DELETE FROM bot_pet_buffs WHERE NOT EXISTS (SELECT * FROM bot_pets WHERE bot_pets.pets_index = bot_pet_buffs.pets_index)" + ); + } + if (!content_db.LoadItems(hotfix_name)) { LogError("Error: Could not load item data. But ignoring"); } diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index dcc5ff89b..a24cd0c30 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -1451,7 +1451,7 @@ bool BotDatabase::DeletePetBuffs(const uint32 bot_id) return true; } - BotPetBuffsRepository::DeleteOne(database, saved_pet_index); + BotPetBuffsRepository::DeleteWhere(database, fmt::format("pets_index = {}", saved_pet_index)); return true; } From b92eafd21b5a86b3e59dc8412f4287056085fb0b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Tue, 22 Oct 2024 00:02:14 -0500 Subject: [PATCH 07/26] [Release] 22.57.1 (#4523) --- CHANGELOG.md | 11 +++++++++++ common/version.h | 2 +- package.json | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e30cf6e..445bec673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [22.57.1] 10/22/2024 + +### Bots + +* Enable Bot Commands Only if Rule Enabled ([#4519](https://github.com/EQEmu/Server/pull/4519)) @Kinglykrab 2024-10-22 +* Fix pet buffs from saving duplicates every save ([#4520](https://github.com/EQEmu/Server/pull/4520)) @nytmyr 2024-10-22 + +### Loginserver + +* Automatic Opcode File Creation ([#4521](https://github.com/EQEmu/Server/pull/4521)) @KimLS 2024-10-22 + ## [22.57.0] 10/20/2024 ### Bots diff --git a/common/version.h b/common/version.h index 6e1c31abc..fbbcd0a7f 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.57.0-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.57.1-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 42a331bc5..f04c26edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.57.0", + "version": "22.57.1", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" From dfd2729b28e653959d31e5ba97841b9a5265ad60 Mon Sep 17 00:00:00 2001 From: Alex King <89047260+Kinglykrab@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:37:21 -0400 Subject: [PATCH 08/26] [Bug Fix] Add Missing Lua Registers (#4525) --- zone/lua_general.cpp | 2 -- zone/lua_general.h | 2 ++ zone/lua_parser.cpp | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index 74ed3a721..eb4c54c2f 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -6772,7 +6772,6 @@ luabind::scope lua_register_random() { )]; } - luabind::scope lua_register_events() { return luabind::class_("Event") .enum_("constants") @@ -8008,7 +8007,6 @@ luabind::scope lua_register_journal_mode() { )]; } - luabind::scope lua_register_exp_source() { return luabind::class_("ExpSource") .enum_("constants") diff --git a/zone/lua_general.h b/zone/lua_general.h index 802e8a055..6c195d817 100644 --- a/zone/lua_general.h +++ b/zone/lua_general.h @@ -21,8 +21,10 @@ luabind::scope lua_register_rules_const(); luabind::scope lua_register_rulei(); luabind::scope lua_register_ruler(); luabind::scope lua_register_ruleb(); +luabind::scope lua_register_rules(); luabind::scope lua_register_journal_speakmode(); luabind::scope lua_register_journal_mode(); +luabind::scope lua_register_exp_source(); #endif #endif diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index b485d02ae..0b3e4913e 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -1312,11 +1312,13 @@ void LuaParser::MapFunctions(lua_State *L) { lua_register_rulei(), lua_register_ruler(), lua_register_ruleb(), + lua_register_rules(), lua_register_journal_speakmode(), lua_register_journal_mode(), lua_register_expedition(), lua_register_expedition_lock_messages(), - lua_register_buff() + lua_register_buff(), + lua_register_exp_source() )]; } catch(std::exception &ex) { From d02d7665636d5b0727c5686427a97b969493a1fe Mon Sep 17 00:00:00 2001 From: Alex King <89047260+Kinglykrab@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:47:02 -0400 Subject: [PATCH 09/26] [Bug Fix] Fix cross_zone_set_entity_variable_by_char_id in Lua (#4526) --- zone/lua_general.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index eb4c54c2f..f8a8a4ee4 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -6583,7 +6583,7 @@ luabind::scope lua_register_general() { luabind::def("cross_zone_reset_activity_by_guild_id", &lua_cross_zone_reset_activity_by_guild_id), luabind::def("cross_zone_reset_activity_by_expedition_id", &lua_cross_zone_reset_activity_by_expedition_id), luabind::def("cross_zone_reset_activity_by_client_name", &lua_cross_zone_reset_activity_by_client_name), - luabind::def("cross_zone_set_entity_variable_by_client_name", &lua_cross_zone_set_entity_variable_by_client_name), + luabind::def("cross_zone_set_entity_variable_by_char_id", &lua_cross_zone_set_entity_variable_by_char_id), luabind::def("cross_zone_set_entity_variable_by_group_id", &lua_cross_zone_set_entity_variable_by_group_id), luabind::def("cross_zone_set_entity_variable_by_raid_id", &lua_cross_zone_set_entity_variable_by_raid_id), luabind::def("cross_zone_set_entity_variable_by_guild_id", &lua_cross_zone_set_entity_variable_by_guild_id), From 41dd8a575479b8214b9878e4ca5b961315f6662f Mon Sep 17 00:00:00 2001 From: Alex King <89047260+Kinglykrab@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:40:25 -0400 Subject: [PATCH 10/26] [Quest API] Add Spawn Circle/Grid Methods to Perl/Lua (#4524) * [Quest API] Add Spawn Circle/Grid Methods to Perl/Lua * Update lua_general.cpp * Update questmgr.cpp * Update questmgr.cpp --- zone/embparser_api.cpp | 12 +++++++ zone/lua_general.cpp | 12 +++++++ zone/questmgr.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++ zone/questmgr.h | 2 ++ 4 files changed, 101 insertions(+) diff --git a/zone/embparser_api.cpp b/zone/embparser_api.cpp index f33759d1a..d18796a4e 100644 --- a/zone/embparser_api.cpp +++ b/zone/embparser_api.cpp @@ -5978,6 +5978,16 @@ bool Perl__aretaskscompleted(perl::array task_ids) return quest_manager.aretaskscompleted(v); } +void Perl__SpawnCircle(uint32 npc_id, float x, float y, float z, float heading, float radius, uint32 points) +{ + quest_manager.SpawnCircle(npc_id, glm::vec4(x, y, z, heading), radius, points); +} + +void Perl__SpawnGrid(uint32 npc_id, float x, float y, float z, float heading, float spacing, uint32 spawn_count) +{ + quest_manager.SpawnGrid(npc_id, glm::vec4(x, y, z, heading), spacing, spawn_count); +} + void perl_register_quest() { perl::interpreter perl(PERL_GET_THX); @@ -6287,6 +6297,8 @@ void perl_register_quest() package.add("SendMail", &Perl__SendMail); package.add("SetAutoLoginCharacterNameByAccountID", &Perl__SetAutoLoginCharacterNameByAccountID); package.add("SetRunning", &Perl__SetRunning); + package.add("SpawnCircle", &Perl__SpawnCircle); + package.add("SpawnGrid", &Perl__SpawnGrid); package.add("activespeakactivity", &Perl__activespeakactivity); package.add("activespeaktask", &Perl__activespeaktask); package.add("activetasksinset", &Perl__activetasksinset); diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index f8a8a4ee4..6567d2476 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -5635,6 +5635,16 @@ int lua_are_tasks_completed(luabind::object task_ids) return quest_manager.aretaskscompleted(v); } +void lua_spawn_circle(uint32 npc_id, float x, float y, float z, float heading, float radius, uint32 points) +{ + quest_manager.SpawnCircle(npc_id, glm::vec4(x, y, z, heading), radius, points); +} + +void lua_spawn_grid(uint32 npc_id, float x, float y, float z, float heading, float spacing, uint32 spawn_count) +{ + quest_manager.SpawnGrid(npc_id, glm::vec4(x, y, z, heading), spacing, spawn_count); +} + #define LuaCreateNPCParse(name, c_type, default_value) do { \ cur = table[#name]; \ if(luabind::type(cur) != LUA_TNIL) { \ @@ -6442,6 +6452,8 @@ luabind::scope lua_register_general() { luabind::def("send_parcel", &lua_send_parcel), luabind::def("get_zone_uptime", &lua_get_zone_uptime), luabind::def("are_tasks_completed", &lua_are_tasks_completed), + luabind::def("spawn_circle", &lua_spawn_circle), + luabind::def("spawn_grid", &lua_spawn_grid), /* Cross Zone */ diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index cf77e503e..68bd859af 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -4623,3 +4623,78 @@ bool QuestManager::SetAutoLoginCharacterNameByAccountID(uint32 account_id, const { return AccountRepository::SetAutoLoginCharacterNameByAccountID(database, account_id, character_name); } + +void QuestManager::SpawnCircle(uint32 npc_id, glm::vec4 position, float radius, uint32 points) +{ + const NPCType* t = content_db.LoadNPCTypesData(npc_id); + if (!t) { + return; + } + + glm::vec4 npc_position = position; + + for (uint32 i = 0; i < points; i++) { + float angle = 2 * M_PI * i / points; + + npc_position.x = position.x + radius * std::cos(angle); + npc_position.y = position.y + radius * std::sin(angle); + + NPC* n = new NPC(t, nullptr, npc_position, GravityBehavior::Water); + + n->FixZ(); + + n->AddLootTable(); + + if (n->DropsGlobalLoot()) { + n->CheckGlobalLootTables(); + } + + entity_list.AddNPC(n, true, true); + } +} + +void QuestManager::SpawnGrid(uint32 npc_id, glm::vec4 position, float spacing, uint32 spawn_count) +{ + const NPCType* t = content_db.LoadNPCTypesData(npc_id); + if (!t) { + return; + } + + glm::vec4 npc_position = position; + + uint32 columns = std::ceil(std::sqrt(spawn_count)); + uint32 rows = std::ceil(spawn_count / columns); + + float total_width = ((columns - 1) * spacing); + float total_height = ((rows - 1) * spacing); + + float start_x = position.x - total_width / 2; + float start_y = position.y - total_height / 2; + + uint32 spawned = 0; + + for (uint32 row = 0; row < rows; row++) { + for (uint32 column = 0; column < columns; column++) { + if (spawned >= spawn_count) { + break; + } + + npc_position.x = start_x + column * spacing; + npc_position.y = start_y + row * spacing; + + NPC* n = new NPC(t, nullptr, npc_position, GravityBehavior::Water); + + n->FixZ(); + + n->AddLootTable(); + + if (n->DropsGlobalLoot()) { + n->CheckGlobalLootTables(); + } + + entity_list.AddNPC(n, true, true); + + spawned++; + } + } +} diff --git a/zone/questmgr.h b/zone/questmgr.h index 463a3baa6..1f872aaf5 100644 --- a/zone/questmgr.h +++ b/zone/questmgr.h @@ -357,6 +357,8 @@ public: void SendChannelMessage(Client* from, const char* to, uint8 channel_number, uint32 guild_id, uint8 language_id, uint8 language_skill, const char* message); std::string GetAutoLoginCharacterNameByAccountID(uint32 account_id); bool SetAutoLoginCharacterNameByAccountID(uint32 account_id, const std::string& character_name); + void SpawnCircle(uint32 npc_id, glm::vec4 position, float radius, uint32 points); + void SpawnGrid(uint32 npc_id, glm::vec4 position, float spacing, uint32 spawn_count); Bot *GetBot() const; Client *GetInitiator() const; From 428cccfa50f232bf62ce12cd06d8b33d7c8c13f2 Mon Sep 17 00:00:00 2001 From: mmcgarvey Date: Thu, 31 Oct 2024 08:13:16 -0400 Subject: [PATCH 11/26] [Feature] Focus Skill Attack Spells (#4528) * Add Rule Spells:AllowFocusOnSkillDamageSpells * Currently, focus mods defaults to 0 when processing spell effect 193. * The default value for this rule is false. * When false, the rule will retain the current default behavior. * When true, the aforementioned focus effects will allow focus effects (185, 459, and 482) to modify spell effect 193. * Removed undesirable whitespace * Update spell_effects.cpp --------- Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com> --- common/ruletypes.h | 1 + zone/spell_effects.cpp | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index a42832527..ad63226bd 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -518,6 +518,7 @@ RULE_BOOL(Spells, SnareOverridesSpeedBonuses, false, "Enabling will allow snares RULE_INT(Spells, TargetedAOEMaxTargets, 4, "Max number of targets a Targeted AOE spell can cast on. Set to 0 for no limit.") RULE_INT(Spells, PointBlankAOEMaxTargets, 0, "Max number of targets a Point-Blank AOE spell can cast on. Set to 0 for no limit.") RULE_INT(Spells, DefaultAOEMaxTargets, 0, "Max number of targets that an AOE spell which does not meet other descriptions can cast on. Set to 0 for no limit.") +RULE_BOOL(Spells, AllowFocusOnSkillDamageSpells, false, "Allow focus effects 185, 459, and 482 to enhance SkillAttack spell effect 193") RULE_CATEGORY_END() RULE_CATEGORY(Combat) diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 93721f8c9..7940a7c8e 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -2416,16 +2416,18 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove break; } + int16 focus = RuleB(Spells, AllowFocusOnSkillDamageSpells) ? caster->GetMeleeDamageMod_SE(spells[spell_id].skill) : 0; + switch(spells[spell_id].skill) { - case EQ::skills::SkillThrowing: - caster->DoThrowingAttackDmg(this, nullptr, nullptr, spells[spell_id].base_value[i],spells[spell_id].limit_value[i], 0, ReuseTime, 0, 0, 4.0f, true); - break; - case EQ::skills::SkillArchery: - caster->DoArcheryAttackDmg(this, nullptr, nullptr, spells[spell_id].base_value[i],spells[spell_id].limit_value[i], 0, ReuseTime, 0, 0, nullptr, 0, 4.0f, true); - break; - default: - caster->DoMeleeSkillAttackDmg(this, spells[spell_id].base_value[i], spells[spell_id].skill, spells[spell_id].limit_value[i], 0, false, ReuseTime); - break; + case EQ::skills::SkillThrowing: + caster->DoThrowingAttackDmg(this, nullptr, nullptr, spells[spell_id].base_value[i],spells[spell_id].limit_value[i], focus, ReuseTime, 0, 0, 4.0f, true); + break; + case EQ::skills::SkillArchery: + caster->DoArcheryAttackDmg(this, nullptr, nullptr, spells[spell_id].base_value[i],spells[spell_id].limit_value[i], focus, ReuseTime, 0, 0, nullptr, 0, 4.0f, true); + break; + default: + caster->DoMeleeSkillAttackDmg(this, spells[spell_id].base_value[i], spells[spell_id].skill, spells[spell_id].limit_value[i], focus, false, ReuseTime); + break; } break; } From 95249889a648f50715e18b3b9ab5f86c22863437 Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:12:17 -0500 Subject: [PATCH 12/26] [Code] Add mysql prepared statement support (#4530) This adds support for using prepared statements for MySQL queries. It is intended for use in a database quest API but it can be used in source with some caveats: - It uses exceptions for error handling instead of returning a fake result that needs checked. Usage must be wrapped in try/catch. - DBcore has a connection mutex which indicates the connection might be shared with other threads. This mutex is locked for certain stmt operations in an attempt to make it safe to use with multi threaded connections. - Prepared statements should only be used on the main thread since the internal logging is not synchronized. - Unlike the current query API which retrieves all results as strings, results are stored in buffers that represent the db field type. Getter functions are available to retrieve values as desired types. --- common/CMakeLists.txt | 2 + common/dbcore.cpp | 6 + common/dbcore.h | 7 + common/mysql_stmt.cpp | 586 ++++++++++++++++++++++++++++++++++++++++++ common/mysql_stmt.h | 221 ++++++++++++++++ 5 files changed, 822 insertions(+) create mode 100644 common/mysql_stmt.cpp create mode 100644 common/mysql_stmt.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 3b06c0e1f..0a1766fc8 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -62,6 +62,7 @@ SET(common_sources mutex.cpp mysql_request_result.cpp mysql_request_row.cpp + mysql_stmt.cpp opcode_map.cpp opcodemgr.cpp packet_dump.cpp @@ -586,6 +587,7 @@ SET(common_headers mutex.h mysql_request_result.h mysql_request_row.h + mysql_stmt.h op_codes.h opcode_dispatch.h opcodemgr.h diff --git a/common/dbcore.cpp b/common/dbcore.cpp index 2fad0f2a7..82a9bc396 100644 --- a/common/dbcore.cpp +++ b/common/dbcore.cpp @@ -7,6 +7,7 @@ #include "timer.h" #include "dbcore.h" +#include "mysql_stmt.h" #include #include @@ -436,3 +437,8 @@ MySQLRequestResult DBcore::QueryDatabaseMulti(const std::string &query) return r; } + +mysql::PreparedStmt DBcore::Prepare(std::string query) +{ + return mysql::PreparedStmt(*mysql, std::move(query), m_mutex); +} diff --git a/common/dbcore.h b/common/dbcore.h index 3cc206012..cefdc6522 100644 --- a/common/dbcore.h +++ b/common/dbcore.h @@ -17,6 +17,8 @@ #define CR_SERVER_GONE_ERROR 2006 #define CR_SERVER_LOST 2013 +namespace mysql { class PreparedStmt; } + class DBcore { public: enum eStatus { @@ -48,6 +50,11 @@ public: } void SetMutex(Mutex *mutex); + // only safe on connections shared with other threads if results buffered + // unsafe to use off main thread due to internal server logging + // throws std::runtime_error on failure + mysql::PreparedStmt Prepare(std::string query); + protected: bool Open( const char *iHost, diff --git a/common/mysql_stmt.cpp b/common/mysql_stmt.cpp new file mode 100644 index 000000000..0c71aa53c --- /dev/null +++ b/common/mysql_stmt.cpp @@ -0,0 +1,586 @@ +#include "mysql_stmt.h" +#include "eqemu_logsys.h" +#include "mutex.h" +#include "timer.h" +#include + +namespace mysql +{ + +void PreparedStmt::StmtDeleter::operator()(MYSQL_STMT* stmt) noexcept +{ + // The connection must be locked when closing the stmt to avoid mysql errors + // in case another thread tries to use it during the close. If the mutex is + // changed to one that throws then exceptions need to be caught here. + LockMutex lock(mutex); + mysql_stmt_close(stmt); +} + +PreparedStmt::PreparedStmt(MYSQL& mysql, std::string query, Mutex* mutex, StmtOptions opts) + : m_stmt(mysql_stmt_init(&mysql), { mutex }), m_query(std::move(query)), m_mutex(mutex), m_options(opts) +{ + LockMutex lock(m_mutex); + if (mysql_stmt_prepare(m_stmt.get(), m_query.c_str(), static_cast(m_query.size())) != 0) + { + ThrowError(fmt::format("Prepare error: {}", GetStmtError())); + } + + m_params.resize(mysql_stmt_param_count(m_stmt.get())); + m_inputs.resize(m_params.size()); +} + +void PreparedStmt::ThrowError(const std::string& error) +{ + LogMySQLError("{}", error); + throw std::runtime_error(error); +} + +std::string PreparedStmt::GetStmtError() +{ + auto err = mysql_stmt_errno(m_stmt.get()); + auto str = mysql_stmt_error(m_stmt.get()); + return fmt::format("({}) [{}] for query [{}]", err, str, m_query); +} + +template +void PreparedStmt::BindInput(size_t index, T value) +{ + if (index >= m_inputs.size()) + { + ThrowError(fmt::format("Cannot bind input, index {} out of range", index)); + } + + impl::Bind& arg = m_inputs[index]; + arg.is_null = std::is_same_v; + + MYSQL_BIND& bind = m_params[index]; + bind.is_unsigned = std::is_unsigned_v; + bind.is_null = &arg.is_null; + bind.length = &arg.length; + + auto old_type = bind.buffer_type; + + if constexpr (std::is_arithmetic_v) + { + if (arg.buffer.size() < sizeof(T)) + { + arg.buffer.resize(std::max(sizeof(T), sizeof(int64_t))); + bind.buffer = arg.buffer.data(); + m_need_bind = true; + } + memcpy(arg.buffer.data(), &value, sizeof(T)); + } + + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_TINY; + } + else if constexpr (std::is_same_v || std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_SHORT; + } + else if constexpr (std::is_same_v || std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_LONG; + } + else if constexpr (std::is_same_v || std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_LONGLONG; + } + else if constexpr (std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_FLOAT; + } + else if constexpr (std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_DOUBLE; + } + else if constexpr (std::is_same_v) + { + bind.buffer_type = MYSQL_TYPE_STRING; + if (arg.buffer.empty() || arg.buffer.size() < value.size()) + { + arg.buffer.resize(static_cast((value.size() + 1) * 1.5)); + bind.buffer = arg.buffer.data(); + bind.buffer_length = static_cast(arg.buffer.size()); + m_need_bind = true; + } + std::copy(value.begin(), value.end(), arg.buffer.begin()); + arg.length = static_cast(value.size()); + } + else if constexpr (!std::is_same_v) + { + static_assert(false_v, "Cannot bind unsupported type"); + } + + if (old_type != bind.buffer_type) + { + m_need_bind = true; + } +} + +void PreparedStmt::BindInput(size_t index, const char* str) +{ + BindInput(index, std::string_view(str)); +} + +void PreparedStmt::BindInput(size_t index, const std::string& str) +{ + BindInput(index, std::string_view(str)); +} + +StmtResult PreparedStmt::Execute() +{ + CheckArgs(0); + return DoExecute(); +} + +StmtResult PreparedStmt::Execute(const std::vector& args) +{ + CheckArgs(args.size()); + for (size_t i = 0; i < args.size(); ++i) + { + std::visit([&](const auto& arg) { BindInput(i, arg); }, args[i]); + } + return DoExecute(); +} + +template +StmtResult PreparedStmt::Execute(const std::vector& args) +{ + CheckArgs(args.size()); + for (size_t i = 0; i < args.size(); ++i) + { + BindInput(i, args[i]); + } + return DoExecute(); +} + +void PreparedStmt::CheckArgs(size_t argc) +{ + if (argc != m_params.size()) + { + ThrowError(fmt::format("Bad arg count (got {}, expected {}) for [{}]", argc, m_params.size(), m_query)); + } +} + +StmtResult PreparedStmt::DoExecute() +{ + BenchTimer timer; + LockMutex lock(m_mutex); + + if (m_need_bind && mysql_stmt_bind_param(m_stmt.get(), m_params.data()) != 0) + { + ThrowError(fmt::format("Bind param error: {}", GetStmtError())); + } + + m_need_bind = false; + + if (mysql_stmt_execute(m_stmt.get()) != 0) + { + ThrowError(fmt::format("Execute error: {}", GetStmtError())); + } + + my_bool attr = m_options.use_max_length; + mysql_stmt_attr_set(m_stmt.get(), STMT_ATTR_UPDATE_MAX_LENGTH, &attr); + + if (m_options.buffer_results && mysql_stmt_store_result(m_stmt.get()) != 0) + { + ThrowError(fmt::format("Store result error: {}", GetStmtError())); + } + + // Result buffers are bound on first execute and re-used if needed + if (m_results.empty()) + { + BindResults(); + } + + StmtResult res(m_stmt.get(), m_results.size()); + + if (m_results.empty()) + { + LogMySQLQuery("{} -- ({} row(s) affected) ({:.6f}s)", m_query, res.RowsAffected(), timer.elapsed()); + } + else + { + LogMySQLQuery("{} -- ({} row(s) returned) ({:.6f}s)", m_query, res.RowCount(), timer.elapsed()); + } + + return res; +} + +void PreparedStmt::BindResults() +{ + MYSQL_RES* res = mysql_stmt_result_metadata(m_stmt.get()); + if (!res) + { + return; // did not produce a result set + } + + MYSQL_FIELD* fields = mysql_fetch_fields(res); + m_columns.resize(mysql_num_fields(res)); + m_results.resize(m_columns.size()); + + for (int i = 0; i < static_cast(m_columns.size()); ++i) + { + impl::BindColumn& col = m_columns[i].m_col; + MYSQL_BIND& bind = m_results[i]; + + col.index = i; + col.name = fields[i].name; + col.buffer_type = fields[i].type; + col.is_unsigned = (fields[i].flags & UNSIGNED_FLAG) != 0; + col.buffer.resize(GetResultBufferSize(fields[i])); + + bind.buffer_type = col.buffer_type; + bind.buffer = col.buffer.data(); + bind.buffer_length = static_cast(col.buffer.size()); + bind.is_unsigned = col.is_unsigned; + bind.is_null = &col.is_null; + bind.length = &col.length; + bind.error = &col.error; + } + + mysql_free_result(res); + + if (!m_results.empty() && mysql_stmt_bind_result(m_stmt.get(), m_results.data()) != 0) + { + ThrowError(fmt::format("Bind result error: {}", GetStmtError())); + } +} + +int PreparedStmt::GetResultBufferSize(const MYSQL_FIELD& field) const +{ + switch (field.type) + { + case MYSQL_TYPE_TINY: + return sizeof(int8_t); + case MYSQL_TYPE_SHORT: + return sizeof(int16_t); + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + return sizeof(int32_t); + case MYSQL_TYPE_LONGLONG: + return sizeof(int64_t); + case MYSQL_TYPE_FLOAT: + return sizeof(float); + case MYSQL_TYPE_DOUBLE: + return sizeof(double); + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + return sizeof(MYSQL_TIME); + default: // if max_length is unavailable for strings buffers are resized on fetch + return field.max_length + 1; // ensure valid buffer created + } +} + +StmtRow PreparedStmt::Fetch() +{ + StmtRow row; + if (!m_columns.empty()) + { + int rc = mysql_stmt_fetch(m_stmt.get()); + if (rc == 1) + { + ThrowError(fmt::format("Fetch error: {}", GetStmtError())); + } + + if (rc != MYSQL_NO_DATA) + { + if (rc == MYSQL_DATA_TRUNCATED) + { + FetchTruncated(); + } + row = StmtRow(m_columns); + } + } + return row; +} + +void PreparedStmt::FetchTruncated() +{ + for (int i = 0; i < static_cast(m_columns.size()); ++i) + { + impl::BindColumn& col = m_columns[i].m_col; + if (col.error) + { + MYSQL_BIND& bind = m_results[i]; + col.buffer.resize(static_cast(col.length * 1.5)); + bind.buffer = col.buffer.data(); + bind.buffer_length = static_cast(col.buffer.size()); + + mysql_stmt_fetch_column(m_stmt.get(), &bind, i, 0); + } + } + + if (mysql_stmt_bind_result(m_stmt.get(), m_results.data()) != 0) + { + ThrowError(fmt::format("Fetch rebind result error: {}", GetStmtError())); + } +} + +// --------------------------------------------------------------------------- + +StmtResult::StmtResult(MYSQL_STMT* stmt, size_t columns) +{ + m_num_cols = static_cast(columns); + m_num_rows = mysql_stmt_num_rows(stmt); // requires buffered results + m_affected = mysql_stmt_affected_rows(stmt); + m_insert_id = mysql_stmt_insert_id(stmt); +} + +// --------------------------------------------------------------------------- + +const StmtColumn* StmtRow::GetColumn(size_t index) const +{ + return index < m_columns.size() ? &m_columns[index] : nullptr; +} + +const StmtColumn* StmtRow::GetColumn(std::string_view name) const +{ + auto it = std::ranges::find_if(m_columns, + [name](const StmtColumn& col) { return col.Name() == name; }); + + return it != m_columns.end() ? &(*it) : nullptr; +} + +std::optional StmtRow::operator[](size_t index) const +{ + return GetStr(index); +} + +std::optional StmtRow::operator[](std::string_view name) const +{ + return GetStr(name); +} + +std::optional StmtRow::GetStr(size_t index) const +{ + const StmtColumn* col = GetColumn(index); + return col ? col->GetStr() : std::nullopt; +} + +std::optional StmtRow::GetStr(std::string_view name) const +{ + const StmtColumn* col = GetColumn(name); + return col ? col->GetStr() : std::nullopt; +} + +template requires std::is_arithmetic_v +std::optional StmtRow::Get(size_t index) const +{ + const StmtColumn* col = GetColumn(index); + return col ? col->Get() : std::nullopt; +} + +template requires std::is_arithmetic_v +std::optional StmtRow::Get(std::string_view name) const +{ + const StmtColumn* col = GetColumn(name); + return col ? col->Get() : std::nullopt; +} + +// --------------------------------------------------------------------------- + +static time_t MakeTime(const MYSQL_TIME& mt) +{ + // buffer mt given in mysql session time zone (assumes local) + std::tm tm{}; + tm.tm_year = mt.year - 1900; + tm.tm_mon = mt.month - 1; + tm.tm_mday = mt.day; + tm.tm_hour = mt.hour; + tm.tm_min = mt.minute; + tm.tm_sec = mt.second; + tm.tm_isdst = -1; + return std::mktime(&tm); +} + +static int MakeSeconds(const MYSQL_TIME& mt) +{ + return (mt.neg ? -1 : 1) * static_cast(mt.hour * 3600 + mt.minute * 60 + mt.second); +} + +static uint64_t MakeBits(std::span data) +{ + // byte stream for bits is in big endian + uint64_t bits = 0; + for (size_t i = 0; i < data.size() && i < sizeof(uint64_t); ++i) + { + bits |= static_cast(data[data.size() - i - 1] & 0xff) << (i * 8); + } + return bits; +} + +template +static T FromString(std::string_view sv) +{ + if constexpr (std::is_same_v) + { + // return false for empty (zero-length) strings + return !sv.empty(); + } + else + { + // non numbers return a zero initialized T (could return nullopt instead) + T value = {}; + std::from_chars(sv.data(), sv.data() + sv.size(), value); + return value; + } +} + +static std::string FormatTime(enum_field_types type, const MYSQL_TIME& mt) +{ + switch (type) + { + case MYSQL_TYPE_TIME: // hhh:mm:ss '-838:59:59' to '838:59:59' + return fmt::format("{}{:02d}:{:02d}:{:02d}", mt.neg ? "-" : "", mt.hour, mt.minute, mt.second); + case MYSQL_TYPE_DATE: // YYYY-MM-DD '1000-01-01' to '9999-12-31' + return fmt::format("{}-{:02d}-{:02d}", mt.year, mt.month, mt.day); + case MYSQL_TYPE_DATETIME: // YYYY-MM-DD hh:mm:ss '1000-01-01 00:00:00' to '9999-12-31 23:59:59' + case MYSQL_TYPE_TIMESTAMP: // YYYY-MM-DD hh:mm:ss '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC + return fmt::format("{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", mt.year, mt.month, mt.day, mt.hour, mt.minute, mt.second); + default: + return std::string(); + } +} + +std::optional StmtColumn::GetStrView() const +{ + if (m_col.is_null) + { + return std::nullopt; + } + + switch (m_col.buffer_type) + { + case MYSQL_TYPE_NEWDECIMAL: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_STRING: + return std::make_optional(reinterpret_cast(m_col.buffer.data()), m_col.length); + default: + return std::nullopt; + } +} + +std::optional StmtColumn::GetStr() const +{ + if (m_col.is_null) + { + return std::nullopt; + } + + switch (m_col.buffer_type) + { + case MYSQL_TYPE_TINY: + return m_col.is_unsigned ? fmt::format_int(BitCast()).c_str() : fmt::format_int(BitCast()).c_str(); + case MYSQL_TYPE_SHORT: + return m_col.is_unsigned ? fmt::format_int(BitCast()).c_str() : fmt::format_int(BitCast()).c_str(); + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + return m_col.is_unsigned ? fmt::format_int(BitCast()).c_str() : fmt::format_int(BitCast()).c_str(); + case MYSQL_TYPE_LONGLONG: + return m_col.is_unsigned ? fmt::format_int(BitCast()).c_str() : fmt::format_int(BitCast()).c_str(); + case MYSQL_TYPE_FLOAT: + return fmt::format("{}", BitCast()); + case MYSQL_TYPE_DOUBLE: + return fmt::format("{}", BitCast()); + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + return FormatTime(m_col.buffer_type, BitCast()); + case MYSQL_TYPE_BIT: + return fmt::format_int(*Get()).c_str(); + case MYSQL_TYPE_NEWDECIMAL: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_STRING: + return std::make_optional(reinterpret_cast(m_col.buffer.data()), m_col.length); + default: + return std::nullopt; + } +} + +template requires std::is_arithmetic_v +std::optional StmtColumn::Get() const +{ + if (m_col.is_null) + { + return std::nullopt; + } + + switch (m_col.buffer_type) + { + case MYSQL_TYPE_TINY: + return m_col.is_unsigned ? static_cast(BitCast()) : static_cast(BitCast()); + case MYSQL_TYPE_SHORT: + return m_col.is_unsigned ? static_cast(BitCast()) : static_cast(BitCast()); + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + return m_col.is_unsigned ? static_cast(BitCast()) : static_cast(BitCast()); + case MYSQL_TYPE_LONGLONG: + return m_col.is_unsigned ? static_cast(BitCast()) : static_cast(BitCast()); + case MYSQL_TYPE_FLOAT: + return static_cast(BitCast()); + case MYSQL_TYPE_DOUBLE: + return static_cast(BitCast()); + case MYSQL_TYPE_TIME: // return as total seconds + return static_cast(MakeSeconds(BitCast())); + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: // return as epoch timestamp + return static_cast(MakeTime(BitCast())); + case MYSQL_TYPE_BIT: + return static_cast(MakeBits({ m_col.buffer.data(), m_col.length })); + case MYSQL_TYPE_NEWDECIMAL: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_STRING: + return FromString({ reinterpret_cast(m_col.buffer.data()), m_col.length }); + default: + return std::nullopt; + } +} + +// --------------------------------------------------------------------------- + +// explicit template instantiations for supported types +template void PreparedStmt::BindInput(size_t, std::string_view); +template void PreparedStmt::BindInput(size_t, std::nullptr_t); +template StmtResult PreparedStmt::Execute(const std::vector&); +template StmtResult PreparedStmt::Execute(const std::vector&); +template StmtResult PreparedStmt::Execute(const std::vector&); + +#define INSTANTIATE(T) \ + template void PreparedStmt::BindInput(size_t, T); \ + template StmtResult PreparedStmt::Execute(const std::vector&); \ + template std::optional StmtRow::Get(size_t) const; \ + template std::optional StmtRow::Get(std::string_view) const; \ + template std::optional StmtColumn::Get() const; + +INSTANTIATE(bool); +INSTANTIATE(int8_t); +INSTANTIATE(uint8_t); +INSTANTIATE(int16_t); +INSTANTIATE(uint16_t); +INSTANTIATE(int32_t); +INSTANTIATE(uint32_t); +INSTANTIATE(int64_t); +INSTANTIATE(uint64_t); +INSTANTIATE(float); +INSTANTIATE(double); + +} // namespace mysql diff --git a/common/mysql_stmt.h b/common/mysql_stmt.h new file mode 100644 index 000000000..eb1483f17 --- /dev/null +++ b/common/mysql_stmt.h @@ -0,0 +1,221 @@ +#pragma once + +#include "mysql.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Mutex; + +namespace mysql +{ + +// support MySQL 8.0.1+ API which removed the my_bool type +#if !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 80001 +using my_bool = bool; +#endif + +template +inline constexpr bool false_v = false; + +namespace impl +{ + +struct Bind +{ + std::vector buffer; + unsigned long length = 0; + my_bool is_null = false; + my_bool error = false; +}; + +struct BindColumn : Bind +{ + int index = 0; + std::string name; + bool is_unsigned = false; + enum_field_types buffer_type = {}; +}; + +} // namespace impl + +// --------------------------------------------------------------------------- + +struct StmtOptions +{ + // Enable buffering (storing) entire result set after executing a statement + bool buffer_results = true; + + // Enable MySQL to update max_length of fields in execute result set (requires buffering) + bool use_max_length = true; +}; + +// --------------------------------------------------------------------------- + +// Holds ownership of bound column value buffer +class StmtColumn +{ +public: + int Index() const { return m_col.index; } + bool IsNull() const { return m_col.is_null; } + bool IsUnsigned() const { return m_col.is_unsigned; } + enum_field_types Type() const { return m_col.buffer_type; } + const std::string& Name() const { return m_col.name; } + + // Get view of column value buffer + std::span GetBuf() const { return { m_col.buffer.data(), m_col.length }; } + + // Get view of column string value. Returns nullopt if value is NULL or not a string + std::optional GetStrView() const; + + // Get column value as string. Returns nullopt if value is NULL or field type unsupported + std::optional GetStr() const; + + // Get column value as numeric T. Returns nullopt if value NULL or field type unsupported + template requires std::is_arithmetic_v + std::optional Get() const; + +private: + // uses memcpy for type punning buffer data to avoid UB with strict aliasing + template + T BitCast() const + { + T val; + assert(sizeof(T) == m_col.length); + memcpy(&val, m_col.buffer.data(), sizeof(T)); + return val; + } + + friend class PreparedStmt; // access to allocate and bind buffers + friend class StmtResult; // access to resize truncated buffers + impl::BindColumn m_col; +}; + +// --------------------------------------------------------------------------- + +// Provides a non-owning view of PreparedStmt column value buffers +// Evaluates false if it does not contain a valid row +class StmtRow +{ +public: + StmtRow() = default; + StmtRow(std::span columns) : m_columns(columns) {}; + + explicit operator bool() const noexcept { return !m_columns.empty(); } + + int ColumnCount() const { return static_cast(m_columns.size()); } + const StmtColumn* GetColumn(size_t index) const; + const StmtColumn* GetColumn(std::string_view name) const; + + // Get specified column value as string + // Returns nullopt if column invalid, value is NULL, or field type unsupported + std::optional operator[](size_t index) const; + std::optional operator[](std::string_view name) const; + std::optional GetStr(size_t index) const; + std::optional GetStr(std::string_view name) const; + + // Get specified column value as numeric T + // Returns nullopt if column invalid, value is NULL, or field type unsupported + template requires std::is_arithmetic_v + std::optional Get(size_t index) const; + + template requires std::is_arithmetic_v + std::optional Get(std::string_view name) const; + + auto begin() const { return m_columns.begin(); } + auto end() const { return m_columns.end(); } + +private: + std::span m_columns; +}; + +// --------------------------------------------------------------------------- + +// Result meta data for an executed prepared statement +class StmtResult +{ +public: + StmtResult() = default; + StmtResult(MYSQL_STMT* stmt, size_t columns); + + int ColumnCount() const { return m_num_cols; } + uint64_t RowCount() const { return m_num_rows; } + uint64_t RowsAffected() const { return m_affected; } + uint64_t LastInsertID() const { return m_insert_id; } + +private: + int m_num_cols = 0; + uint64_t m_num_rows = 0; + uint64_t m_affected = 0; + uint64_t m_insert_id = 0; +}; + +// --------------------------------------------------------------------------- + +class PreparedStmt +{ +public: + // Supported argument types for execute + using param_t = std::variant; + + PreparedStmt() = delete; + PreparedStmt(MYSQL& mysql, std::string query, Mutex* mutex, StmtOptions opts = {}); + + const std::string& GetQuery() const { return m_query; } + StmtOptions GetOptions() const { return m_options; } + void SetOptions(StmtOptions options) { m_options = options; } + void FreeResult() { mysql_stmt_free_result(m_stmt.get()); } + + // Execute the prepared statement with specified arguments + // Throws exception on error + template + StmtResult Execute(const std::vector& args); + StmtResult Execute(const std::vector& args); + StmtResult Execute(); + + // Fetch the next row into column buffers (overwrites previous row values) + // Return value evaluates false if no more rows to fetch + // Throws exception on error + StmtRow Fetch(); + +private: + void CheckArgs(size_t argc); + StmtResult DoExecute(); + void BindResults(); + void FetchTruncated(); + int GetResultBufferSize(const MYSQL_FIELD& field) const; + void ThrowError(const std::string& error); + std::string GetStmtError(); + + // bind an input value to a query parameter by index + template + void BindInput(size_t index, T value); + void BindInput(size_t index, const char* str); + void BindInput(size_t index, const std::string& str); + + struct StmtDeleter + { + Mutex* mutex = nullptr; + void operator()(MYSQL_STMT* stmt) noexcept; + }; + +private: + std::unique_ptr m_stmt; + std::vector m_params; // input binds + std::vector m_results; // result binds + std::vector m_inputs; // execute buffers (addresses bound) + std::vector m_columns; // fetch buffers (addresses bound) + std::string m_query; + StmtOptions m_options = {}; + bool m_need_bind = true; + Mutex* m_mutex = nullptr; // connection mutex +}; + +} // namespace mysql From 25ef3d2cdb84e62ea06fc4e1917931a193a611dd Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:14:29 -0500 Subject: [PATCH 13/26] [Code] Update perlbind to 1.1.0 (#4529) - Adds a perl::ref alias for perl::reference - Optimizes array return pushes by accessing SV* values directly instead of using operator[] scalar_proxy - Allows functions with a perl::hash parameter to accept hashes with any scalar key type instead of requiring explicit string keys e.g., foo(123 => 1) will now work on functions accepting a perl::hash --- libs/perlbind/include/perlbind/scalar.h | 2 ++ libs/perlbind/include/perlbind/stack_push.h | 7 ++++--- libs/perlbind/include/perlbind/stack_read.h | 2 +- libs/perlbind/include/perlbind/version.h | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/perlbind/include/perlbind/scalar.h b/libs/perlbind/include/perlbind/scalar.h index ea2d97ade..ab54c17b2 100644 --- a/libs/perlbind/include/perlbind/scalar.h +++ b/libs/perlbind/include/perlbind/scalar.h @@ -251,4 +251,6 @@ private: scalar m_value; }; +using ref = reference; + } // namespace perlbind diff --git a/libs/perlbind/include/perlbind/stack_push.h b/libs/perlbind/include/perlbind/stack_push.h index 61a514cce..2a9830c24 100644 --- a/libs/perlbind/include/perlbind/stack_push.h +++ b/libs/perlbind/include/perlbind/stack_push.h @@ -28,8 +28,8 @@ struct pusher ++m_pushed; } void push(const std::string& value) { mPUSHp(value.c_str(), value.size()); ++m_pushed; } - void push(scalar value) { mPUSHs(value.release()); ++m_pushed; }; - void push(reference value) { mPUSHs(value.release()); ++m_pushed; }; + void push(scalar value) { mPUSHs(value.release()); ++m_pushed; } + void push(reference value) { mPUSHs(value.release()); ++m_pushed; } void push(array value) { @@ -38,7 +38,8 @@ struct pusher for (int i = 0; i < count; ++i) { // mortalizes one reference to array element to avoid copying - PUSHs(sv_2mortal(SvREFCNT_inc(value[i].sv()))); + SV** sv = av_fetch(static_cast(value), i, true); + mPUSHs(SvREFCNT_inc(*sv)); } m_pushed += count; } diff --git a/libs/perlbind/include/perlbind/stack_read.h b/libs/perlbind/include/perlbind/stack_read.h index fe5794124..095436d1d 100644 --- a/libs/perlbind/include/perlbind/stack_read.h +++ b/libs/perlbind/include/perlbind/stack_read.h @@ -242,7 +242,7 @@ struct read_as static bool check(PerlInterpreter* my_perl, int i, int ax, int items) { int remaining = items - i; - return remaining > 0 && remaining % 2 == 0 && SvTYPE(ST(i)) == SVt_PV; + return remaining > 0 && remaining % 2 == 0 && SvTYPE(ST(i)) < SVt_PVAV; } static hash get(PerlInterpreter* my_perl, int i, int ax, int items) diff --git a/libs/perlbind/include/perlbind/version.h b/libs/perlbind/include/perlbind/version.h index bc6f6e3d8..68aee2773 100644 --- a/libs/perlbind/include/perlbind/version.h +++ b/libs/perlbind/include/perlbind/version.h @@ -1,7 +1,7 @@ #pragma once constexpr int perlbind_version_major = 1; -constexpr int perlbind_version_minor = 0; +constexpr int perlbind_version_minor = 1; constexpr int perlbind_version_patch = 0; constexpr int perlbind_version() From 1ce51ca3b07f8d7328df9ce859a301c94abdea03 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Tue, 5 Nov 2024 22:02:32 -0600 Subject: [PATCH 14/26] [Release] 22.58.0 (#4532) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ common/version.h | 2 +- package.json | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 445bec673..90dfe1041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [22.58.0] 11/5/2024 + +### Code + +* Add mysql prepared statement support ([#4530](https://github.com/EQEmu/Server/pull/4530)) @hgtw 2024-11-06 +* Update perlbind to 1.1.0 ([#4529](https://github.com/EQEmu/Server/pull/4529)) @hgtw 2024-11-06 + +### Feature + +* Focus Skill Attack Spells ([#4528](https://github.com/EQEmu/Server/pull/4528)) @mmcgarvey 2024-10-31 + +### Fixes + +* Add Missing Lua Registers ([#4525](https://github.com/EQEmu/Server/pull/4525)) @Kinglykrab 2024-10-24 +* Fix cross_zone_set_entity_variable_by_char_id in Lua ([#4526](https://github.com/EQEmu/Server/pull/4526)) @Kinglykrab 2024-10-24 + +### Loginserver + +* Automatifc Opcode File Creation ([#4521](https://github.com/EQEmu/Server/pull/4521)) @KimLS 2024-10-22 + +### Quest API + +* Add Spawn Circle/Grid Methods to Perl/Lua ([#4524](https://github.com/EQEmu/Server/pull/4524)) @Kinglykrab 2024-10-24 + ## [22.57.1] 10/22/2024 ### Bots diff --git a/common/version.h b/common/version.h index fbbcd0a7f..f7a44bb87 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.57.1-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.58.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 f04c26edd..8ff6f1581 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.57.1", + "version": "22.58.0", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" From 0ea47fadeedeadbd1535259cda5eae6f5d0e06cb Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 8 Nov 2024 17:48:39 -0600 Subject: [PATCH 15/26] [Performance] Improvements to ScanCloseMobs logic (#4534) * [Performance] Minor improvements to ScanCloseMobs * Remove timer checks one level up to reduce branching * Reserve memory in m_close_mobs to avoid frequent re-allocations if not already reserved. --- zone/bot.cpp | 5 ++++- zone/client_packet.cpp | 6 +++++- zone/client_process.cpp | 4 +++- zone/entity.cpp | 30 +++++++++++------------------- zone/mob.cpp | 34 ++++++++++++---------------------- zone/mob.h | 1 - zone/npc.cpp | 9 +++++++-- 7 files changed, 42 insertions(+), 47 deletions(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index ce7edcc82..7871f9ac9 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -1578,7 +1578,10 @@ bool Bot::Process() return false; } - ScanCloseMobProcess(); + if (m_scan_close_mobs_timer.Check()) { + entity_list.ScanCloseMobs(this); + } + SpellProcess(); if (tic_timer.Check()) { diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index c880e310f..83af9b13d 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -5013,7 +5013,11 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { SetMoving(!(cy == m_Position.y && cx == m_Position.x)); CheckClientToNpcAggroTimer(); - CheckScanCloseMobsMovingTimer(); + + if (m_mob_check_moving_timer.Check()) { + CheckScanCloseMobsMovingTimer(); + } + CheckSendBulkClientPositionUpdate(); int32 new_animation = ppu->animation; diff --git a/zone/client_process.cpp b/zone/client_process.cpp index d0dab4c27..c2e20fe5b 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -281,7 +281,9 @@ bool Client::Process() { } } - ScanCloseMobProcess(); + if (m_scan_close_mobs_timer.Check()) { + entity_list.ScanCloseMobs(this); + } if (RuleB(Inventory, LazyLoadBank)) { // poll once a second to see if we are close to a banker and we haven't loaded the bank yet diff --git a/zone/entity.cpp b/zone/entity.cpp index c21856d37..d4b3ff72f 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2947,6 +2947,12 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob) { float scan_range = RuleI(Range, MobCloseScanDistance) * RuleI(Range, MobCloseScanDistance); + // Reserve memory in m_close_mobs to avoid frequent re-allocations if not already reserved. + // Assuming mob_list.size() as an upper bound for reservation. + if (scanning_mob->m_close_mobs.bucket_count() < mob_list.size()) { + scanning_mob->m_close_mobs.reserve(mob_list.size()); + } + scanning_mob->m_close_mobs.clear(); for (auto &e : mob_list) { @@ -2957,28 +2963,14 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob) float distance = DistanceSquared(scanning_mob->GetPosition(), mob->GetPosition()); if (distance <= scan_range || mob->GetAggroRange() >= scan_range) { - scanning_mob->m_close_mobs.emplace(std::pair(mob->GetID(), mob)); - - // add self to other mobs close list - if (scanning_mob->GetID() > 0) { - bool has_mob = false; - - for (auto &cm: mob->m_close_mobs) { - if (scanning_mob->GetID() == cm.first) { - has_mob = true; - break; - } - } - - if (!has_mob) { - mob->m_close_mobs.insert(std::pair(scanning_mob->GetID(), scanning_mob)); - } - } + // add mob to scanning_mob's close list and vice versa + mob->m_close_mobs[scanning_mob->GetID()] = scanning_mob; + scanning_mob->m_close_mobs[mob->GetID()] = mob; } } - LogAIScanCloseDetail( - "[{}] Scanning Close List | list_size [{}] moving [{}]", + LogAIScanClose( + "[{}] Scanning close list > list_size [{}] moving [{}]", scanning_mob->GetCleanName(), scanning_mob->m_close_mobs.size(), scanning_mob->IsMoving() ? "true" : "false" diff --git a/zone/mob.cpp b/zone/mob.cpp index a1d9b5d38..2b43ff5e2 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -8584,6 +8584,7 @@ bool Mob::HasBotAttackFlag(Mob* tar) { const uint16 scan_close_mobs_timer_moving = 6000; // 6 seconds const uint16 scan_close_mobs_timer_idle = 60000; // 60 seconds +// If the moving timer triggers, lets see if we are moving or idle to restart the appropriate dynamic timer void Mob::CheckScanCloseMobsMovingTimer() { LogAIScanCloseDetail( @@ -8593,31 +8594,20 @@ void Mob::CheckScanCloseMobsMovingTimer() m_scan_close_mobs_timer.GetRemainingTime() ); - // If the moving timer triggers, lets see if we are moving or idle to restart the appropriate - // dynamic timer - if (m_mob_check_moving_timer.Check()) { - // If the mob is still moving, restart the moving timer - if (moving) { - if (m_scan_close_mobs_timer.GetRemainingTime() > scan_close_mobs_timer_moving) { - LogAIScanCloseDetail("Mob [{}] Restarting with moving timer", GetCleanName()); - m_scan_close_mobs_timer.Disable(); - m_scan_close_mobs_timer.Start(scan_close_mobs_timer_moving); - m_scan_close_mobs_timer.Trigger(); - } - } - // If the mob is not moving, restart the idle timer - else if (m_scan_close_mobs_timer.GetDuration() == scan_close_mobs_timer_moving) { - LogAIScanCloseDetail("Mob [{}] Restarting with idle timer", GetCleanName()); + // If the mob is still moving, restart the moving timer + if (moving) { + if (m_scan_close_mobs_timer.GetRemainingTime() > scan_close_mobs_timer_moving) { + LogAIScanCloseDetail("Mob [{}] Restarting with moving timer", GetCleanName()); m_scan_close_mobs_timer.Disable(); - m_scan_close_mobs_timer.Start(scan_close_mobs_timer_idle); + m_scan_close_mobs_timer.Start(scan_close_mobs_timer_moving); + m_scan_close_mobs_timer.Trigger(); } } -} - -void Mob::ScanCloseMobProcess() -{ - if (m_scan_close_mobs_timer.Check()) { - entity_list.ScanCloseMobs(this); + // If the mob is not moving, restart the idle timer + else if (m_scan_close_mobs_timer.GetDuration() == scan_close_mobs_timer_moving) { + LogAIScanCloseDetail("Mob [{}] Restarting with idle timer", GetCleanName()); + m_scan_close_mobs_timer.Disable(); + m_scan_close_mobs_timer.Start(scan_close_mobs_timer_idle); } } diff --git a/zone/mob.h b/zone/mob.h index 8914534cb..d1bf386d6 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1488,7 +1488,6 @@ public: bool IsCloseToBanker(); - void ScanCloseMobProcess(); std::unordered_map &GetCloseMobList(float distance = 0.0f); void CheckScanCloseMobsMovingTimer(); diff --git a/zone/npc.cpp b/zone/npc.cpp index 2172a529e..f65bd7fad 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -601,8 +601,13 @@ bool NPC::Process() DepopSwarmPets(); } - ScanCloseMobProcess(); - CheckScanCloseMobsMovingTimer(); + if (m_scan_close_mobs_timer.Check()) { + entity_list.ScanCloseMobs(this); + } + + if (m_mob_check_moving_timer.Check()) { + CheckScanCloseMobsMovingTimer(); + } if (hp_regen_per_second > 0 && hp_regen_per_second_timer.Check()) { if (GetHP() < GetMaxHP()) { From 20d3ab2ac588965c3cdfa528811b4491a131f13b Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:15:12 -0400 Subject: [PATCH 16/26] [Bug Fix] Bazaar two edge case issues resolved (#4533) This update resolves two bazaar issues that have been reported. - If parcel delivery is used to purchase an item, and the seller has several of the same items, that have various charges, the item would not be removed from the db. This allowed for incorrect purchases. - If a player 'reclaims' an alt currency item that they also have for sale with an active trader, the item would remain for sale, and be reclaimed. This impacted custom alt currency items that were no trade. --- zone/client_packet.cpp | 8 ++++++++ zone/trading.cpp | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 83af9b13d..14ee222f3 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -2731,6 +2731,14 @@ void Client::Handle_OP_AltCurrencyReclaim(const EQApplicationPacket *app) return; } + if (IsTrader()) { + TraderEndTrader(); + } + + if (IsBuyer()) { + ToggleBuyerMode(false); + } + /* Item to Currency Storage */ if (reclaim->reclaim_flag == 1) { uint32 removed = NukeItem(item_id, invWhereWorn | invWherePersonal | invWhereCursor); diff --git a/zone/trading.cpp b/zone/trading.cpp index ac36f014f..ac142319e 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -3497,7 +3497,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati ps.item_slot = parcel_out.slot_id; strn0cpy(ps.send_to, GetCleanName(), sizeof(ps.send_to)); - if (trader_item.item_charges <= static_cast(tbs->quantity)) { + if (trader_item.item_charges <= static_cast(tbs->quantity) || !buy_item->IsStackable()) { TraderRepository::DeleteOne(database, trader_item.id); } else { TraderRepository::UpdateQuantity( From 77de9619b51e5d4d7a4d22e77a28673bfe0ab382 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 8 Nov 2024 21:26:00 -0600 Subject: [PATCH 17/26] [Databuckets] Add database index to data_buckets (#4535) * [Databuckets] Add database index to data_buckets * Update database_update_manifest.cpp --- common/database/database_update_manifest.cpp | 12 ++++++++++++ common/version.h | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 63f3ba35b..e09a24664 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5758,6 +5758,18 @@ ALTER TABLE `inventory_snapshots` ALTER TABLE `character_exp_modifiers` MODIFY COLUMN `aa_modifier` float NOT NULL DEFAULT 1.0 AFTER `instance_version`, MODIFY COLUMN `exp_modifier` float NOT NULL DEFAULT 1.0 AFTER `aa_modifier`; +)" + }, + ManifestEntry{ + .version = 9285, + .description = "2024_11_08_data_buckets_indexes.sql", + .check = "SHOW CREATE TABLE `data_buckets`", + .condition = "contains", + .match = "idx_character_expires", + .sql = R"( +CREATE INDEX idx_character_expires ON data_buckets (character_id, expires); +CREATE INDEX idx_npc_expires ON data_buckets (npc_id, expires); +CREATE INDEX idx_bot_expires ON data_buckets (bot_id, expires); )" } // -- template; copy/paste this when you need to create a new entry diff --git a/common/version.h b/common/version.h index f7a44bb87..708dbe9d2 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9284 +#define CURRENT_BINARY_DATABASE_VERSION 9285 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9045 #endif From 3f0f95976c05c3ccf4736c3fd876f83ee644dca6 Mon Sep 17 00:00:00 2001 From: Akkadius Date: Sun, 10 Nov 2024 06:47:42 -0600 Subject: [PATCH 18/26] [Hotfix] ScanCloseMobs - Ensure scanning mob has an entity ID --- zone/entity.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zone/entity.cpp b/zone/entity.cpp index d4b3ff72f..f53d4a723 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2945,6 +2945,14 @@ void EntityList::RemoveAuraFromMobs(Mob *aura) // entity list (zone wide) void EntityList::ScanCloseMobs(Mob *scanning_mob) { + if (!scanning_mob) { + return; + } + + if (scanning_mob->GetID() <= 0) { + return; + } + float scan_range = RuleI(Range, MobCloseScanDistance) * RuleI(Range, MobCloseScanDistance); // Reserve memory in m_close_mobs to avoid frequent re-allocations if not already reserved. From 011e1d05e72b796cf9d0c43e90e11abfda37dedc Mon Sep 17 00:00:00 2001 From: Akkadius Date: Sun, 10 Nov 2024 23:19:40 -0600 Subject: [PATCH 19/26] [Hotfix] Check if the mob is already in the close mobs list before inserting --- zone/entity.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zone/entity.cpp b/zone/entity.cpp index f53d4a723..4a9a232ca 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2972,7 +2972,10 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob) float distance = DistanceSquared(scanning_mob->GetPosition(), mob->GetPosition()); if (distance <= scan_range || mob->GetAggroRange() >= scan_range) { // add mob to scanning_mob's close list and vice versa - mob->m_close_mobs[scanning_mob->GetID()] = scanning_mob; + // check if the mob is already in the close mobs list before inserting + if (mob->m_close_mobs.find(scanning_mob->GetID()) == mob->m_close_mobs.end()) { + mob->m_close_mobs[scanning_mob->GetID()] = scanning_mob; + } scanning_mob->m_close_mobs[mob->GetID()] = mob; } } From c1df3fbcb022b98fd3177278f6705f60c46677c3 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 12 Nov 2024 08:00:22 -0800 Subject: [PATCH 20/26] [Rules] Add Rule for restricting client versions to world server (#4527) * add rule for supported clients, unsupported client packet * whitespace * PR feedback - Update client.cpp * PR Feedback - Update client.cpp * Update client.cpp * Update client.cpp --------- Co-authored-by: Paul Johnson --- common/ruletypes.h | 1 + world/client.cpp | 55 +++++++++++++++++++++++++++++++++++++++++++--- world/client.h | 1 + 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index ad63226bd..b2de14baa 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -339,6 +339,7 @@ RULE_STRING(World, MOTD, "", "Server MOTD sent on login, change from empty to ha RULE_STRING(World, Rules, "", "Server Rules, change from empty to have this be used instead of variables table 'rules' value, lines are pipe (|) separated, example: A|B|C") RULE_BOOL(World, EnableAutoLogin, false, "Enables or disables auto login of characters, allowing people to log characters in directly from loginserver to ingame") RULE_BOOL(World, EnablePVPRegions, true, "Enables or disables PVP Regions automatically setting your PVP flag") +RULE_STRING(World, SupportedClients, "", "Comma-delimited list of clients to restrict to. Supported values are Titanium | SoF | SoD | UF | RoF | RoF2. Example: Titanium,RoF2") RULE_CATEGORY_END() RULE_CATEGORY(Zone) diff --git a/world/client.cpp b/world/client.cpp index bcb9100a3..a6f5cfb5f 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -526,9 +526,27 @@ bool Client::HandleSendLoginInfoPacket(const EQApplicationPacket *app) SendEnterWorld(cle->name()); SendPostEnterWorld(); if (!is_player_zoning) { - SendExpansionInfo(); - SendCharInfo(); - database.LoginIP(cle->AccountID(), long2ip(GetIP())); + const auto supported_clients = RuleS(World, SupportedClients); + bool skip_char_info = false; + if (!supported_clients.empty()) { + const std::string& name = EQ::versions::ClientVersionName(m_ClientVersion); + const auto& clients = Strings::Split(supported_clients, ","); + if (std::find(clients.begin(), clients.end(), name) == clients.end()) { + SendUnsupportedClientPacket( + fmt::format( + "Client Not In Supported List [{}]", + supported_clients + ) + ); + skip_char_info = true; + } + } + + if (!skip_char_info) { + SendExpansionInfo(); + SendCharInfo(); + database.LoginIP(cle->AccountID(), long2ip(GetIP())); + } } cle->SetIP(GetIP()); @@ -2453,3 +2471,34 @@ void Client::SendGuildTributeOptInToggle(const GuildTributeMemberToggle *in) QueuePacket(outapp); safe_delete(outapp); } + +void Client::SendUnsupportedClientPacket(const std::string& message) +{ + EQApplicationPacket packet(OP_SendCharInfo, sizeof(CharacterSelect_Struct) + sizeof(CharacterSelectEntry_Struct)); + + unsigned char* buff_ptr = packet.pBuffer; + auto cs = (CharacterSelect_Struct*) buff_ptr; + + cs->CharCount = 1; + cs->TotalChars = 1; + + buff_ptr += sizeof(CharacterSelect_Struct); + + auto e = (CharacterSelectEntry_Struct*) buff_ptr; + + strcpy(e->Name, message.c_str()); + + e->Race = Race::Human; + e->Class = Class::Warrior; + e->Level = 1; + e->ShroudClass = e->Class; + e->ShroudRace = e->Race; + e->Zone = std::numeric_limits::max(); + e->Instance = 0; + e->Gender = Gender::Male; + e->GoHome = 0; + e->Tutorial = 0; + e->Enabled = 0; + + QueuePacket(&packet); +} diff --git a/world/client.h b/world/client.h index b1f0afe8e..e88f8a94e 100644 --- a/world/client.h +++ b/world/client.h @@ -120,6 +120,7 @@ private: EQStreamInterface* eqs; bool CanTradeFVNoDropItem(); void RecordPossibleHack(const std::string& message); + void SendUnsupportedClientPacket(const std::string& message); }; bool CheckCharCreateInfoSoF(CharCreate_Struct *cc); From bcedfe7032531fbd4562596b1733c5fefba34aac Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:01:18 -0500 Subject: [PATCH 21/26] [Quest API] Add Native Database Querying Interface (#4531) * Add database quest API API functions are named to be similar to LuaSQL and perl DBI New connections are made for Database objects. These can either use credentials from the server eqemu_config or manual connections. * Add option to use zone db connections --- zone/CMakeLists.txt | 6 + zone/embparser.cpp | 2 + zone/embperl.h | 2 + zone/lua_database.cpp | 214 ++++++++++++++++++++++++++++++++++ zone/lua_database.h | 51 +++++++++ zone/lua_parser.cpp | 4 +- zone/perl_database.cpp | 255 +++++++++++++++++++++++++++++++++++++++++ zone/perl_database.h | 50 ++++++++ zone/quest_db.cpp | 57 +++++++++ zone/quest_db.h | 30 +++++ 10 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 zone/lua_database.cpp create mode 100644 zone/lua_database.h create mode 100644 zone/perl_database.cpp create mode 100644 zone/perl_database.h create mode 100644 zone/quest_db.cpp create mode 100644 zone/quest_db.h diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 238dcf9f0..1ead62e1b 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -54,6 +54,7 @@ SET(zone_sources lua_buff.cpp lua_corpse.cpp lua_client.cpp + lua_database.cpp lua_door.cpp lua_encounter.cpp lua_entity.cpp @@ -110,6 +111,7 @@ SET(zone_sources perl_bot.cpp perl_buff.cpp perl_client.cpp + perl_database.cpp perl_doors.cpp perl_entity.cpp perl_expedition.cpp @@ -135,6 +137,7 @@ SET(zone_sources qglobals.cpp queryserv.cpp questmgr.cpp + quest_db.cpp quest_parser_collection.cpp raids.cpp raycast_mesh.cpp @@ -215,6 +218,7 @@ SET(zone_headers lua_buff.h lua_client.h lua_corpse.h + lua_database.h lua_door.h lua_encounter.h lua_entity.h @@ -251,6 +255,7 @@ SET(zone_headers pathfinder_interface.h pathfinder_nav_mesh.h pathfinder_null.h + perl_database.h perlpacket.h petitions.h pets.h @@ -260,6 +265,7 @@ SET(zone_headers queryserv.h quest_interface.h questmgr.h + quest_db.h quest_parser_collection.h raids.h raycast_mesh.h diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 8d4145c67..113374e81 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -58,6 +58,7 @@ void perl_register_expedition_lock_messages(); void perl_register_bot(); void perl_register_buff(); void perl_register_merc(); +void perl_register_database(); #endif // EMBPERL_XS_CLASSES #endif // EMBPERL_XS @@ -1185,6 +1186,7 @@ void PerlembParser::MapFunctions() perl_register_bot(); perl_register_buff(); perl_register_merc(); + perl_register_database(); #endif // EMBPERL_XS_CLASSES } diff --git a/zone/embperl.h b/zone/embperl.h index 9fe757184..022971059 100644 --- a/zone/embperl.h +++ b/zone/embperl.h @@ -21,6 +21,8 @@ Eglin #include namespace perl = perlbind; +#undef connect +#undef bind #undef Null #ifdef WIN32 diff --git a/zone/lua_database.cpp b/zone/lua_database.cpp new file mode 100644 index 000000000..aa2a5bea4 --- /dev/null +++ b/zone/lua_database.cpp @@ -0,0 +1,214 @@ +#ifdef LUA_EQEMU + +#include "lua_database.h" +#include "zonedb.h" +#include +#include + +// Luabind adopts the PreparedStmt wrapper object allocated with new and deletes it via GC +// Lua GC is non-deterministic so handles should be closed explicitly to free db resources +// Script errors/exceptions will hold resources until GC deletes the wrapper object + +Lua_MySQLPreparedStmt* Lua_Database::Prepare(lua_State* L, std::string query) +{ + return m_db ? new Lua_MySQLPreparedStmt(L, m_db->Prepare(std::move(query))) : nullptr; +} + +void Lua_Database::Close() +{ + m_db.reset(); +} + +// --------------------------------------------------------------------------- + +void Lua_MySQLPreparedStmt::Close() +{ + m_stmt.reset(); +} + +void Lua_MySQLPreparedStmt::Execute(lua_State* L) +{ + if (m_stmt) + { + m_res = m_stmt->Execute(); + } +} + +void Lua_MySQLPreparedStmt::Execute(lua_State* L, luabind::object args) +{ + if (m_stmt) + { + std::vector inputs; + + // iterate table until nil like ipairs to guarantee traversal order + for (int i = 1, type; (type = luabind::type(args[i])) != LUA_TNIL; ++i) + { + switch (type) + { + case LUA_TBOOLEAN: + inputs.emplace_back(luabind::object_cast(args[i])); + break; + case LUA_TNUMBER: // all numbers are doubles in lua before 5.3 + inputs.emplace_back(luabind::object_cast(args[i])); + break; + case LUA_TSTRING: + inputs.emplace_back(luabind::object_cast(args[i])); + break; + case LUA_TTABLE: // let tables substitute for null since nils can't exist + inputs.emplace_back(nullptr); + break; + default: + break; + } + } + + m_res = m_stmt->Execute(inputs); + } +} + +void Lua_MySQLPreparedStmt::SetOptions(luabind::object table) +{ + if (m_stmt) + { + mysql::StmtOptions opts = m_stmt->GetOptions(); + if (luabind::type(table["buffer_results"]) == LUA_TBOOLEAN) + { + opts.buffer_results = luabind::object_cast(table["buffer_results"]); + } + if (luabind::type(table["use_max_length"]) == LUA_TBOOLEAN) + { + opts.use_max_length = luabind::object_cast(table["use_max_length"]); + } + m_stmt->SetOptions(opts); + } +} + +static void PushValue(lua_State* L, const mysql::StmtColumn& col) +{ + if (col.IsNull()) + { + lua_pushnil(L); // clear entry in cache from any previous row + return; + } + + // 64-bit ints are pushed as strings since lua 5.1 only has 53-bit precision + switch (col.Type()) + { + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + lua_pushnumber(L, col.Get().value()); + break; + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_BIT: + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + std::string str = col.GetStr().value(); + lua_pushlstring(L, str.data(), str.size()); + } + break; + default: // string types, push raw buffer to avoid copy + { + std::string_view str = col.GetStrView().value(); + lua_pushlstring(L, str.data(), str.size()); + } + break; + } +} + +luabind::object Lua_MySQLPreparedStmt::FetchArray(lua_State* L) +{ + auto row = m_stmt ? m_stmt->Fetch() : mysql::StmtRow(); + if (!row) + { + return luabind::object(); + } + + // perf: bypass luabind operator[] + m_row_array.push(L); + for (const mysql::StmtColumn& col : row) + { + PushValue(L, col); + lua_rawseti(L, -2, col.Index() + 1); + } + lua_pop(L, 1); + + return m_row_array; +} + +luabind::object Lua_MySQLPreparedStmt::FetchHash(lua_State* L) +{ + auto row = m_stmt ? m_stmt->Fetch() : mysql::StmtRow(); + if (!row) + { + return luabind::object(); + } + + // perf: bypass luabind operator[] + m_row_hash.push(L); + for (const mysql::StmtColumn& col : row) + { + PushValue(L, col); + lua_setfield(L, -2, col.Name().c_str()); + } + lua_pop(L, 1); + + return m_row_hash; +} + +int Lua_MySQLPreparedStmt::ColumnCount() +{ + return m_res.ColumnCount(); +} + +uint64_t Lua_MySQLPreparedStmt::LastInsertID() +{ + return m_res.LastInsertID(); +} + +uint64_t Lua_MySQLPreparedStmt::RowCount() +{ + return m_res.RowCount(); +} + +uint64_t Lua_MySQLPreparedStmt::RowsAffected() +{ + return m_res.RowsAffected(); +} + +luabind::scope lua_register_database() +{ + return luabind::class_("Database") + .enum_("constants") + [( + luabind::value("Default", static_cast(QuestDB::Connection::Default)), + luabind::value("Content", static_cast(QuestDB::Connection::Content)) + )] + .def(luabind::constructor<>()) + .def(luabind::constructor()) + .def(luabind::constructor()) + .def(luabind::constructor()) + .def("close", &Lua_Database::Close) + .def("prepare", &Lua_Database::Prepare, luabind::adopt(luabind::result)), + + luabind::class_("MySQLPreparedStmt") + .def("close", &Lua_MySQLPreparedStmt::Close) + .def("execute", static_cast(&Lua_MySQLPreparedStmt::Execute)) + .def("execute", static_cast(&Lua_MySQLPreparedStmt::Execute)) + .def("fetch", &Lua_MySQLPreparedStmt::FetchArray) + .def("fetch_array", &Lua_MySQLPreparedStmt::FetchArray) + .def("fetch_hash", &Lua_MySQLPreparedStmt::FetchHash) + .def("insert_id", &Lua_MySQLPreparedStmt::LastInsertID) + .def("num_fields", &Lua_MySQLPreparedStmt::ColumnCount) + .def("num_rows", &Lua_MySQLPreparedStmt::RowCount) + .def("rows_affected", &Lua_MySQLPreparedStmt::RowsAffected) + .def("set_options", &Lua_MySQLPreparedStmt::SetOptions); +} + +#endif // LUA_EQEMU diff --git a/zone/lua_database.h b/zone/lua_database.h new file mode 100644 index 000000000..ca81f67cd --- /dev/null +++ b/zone/lua_database.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef LUA_EQEMU + +#include "quest_db.h" +#include "../common/mysql_stmt.h" +#include + +namespace luabind { struct scope; } +luabind::scope lua_register_database(); + +class Lua_MySQLPreparedStmt; + +class Lua_Database : public QuestDB +{ +public: + using QuestDB::QuestDB; + + void Close(); + Lua_MySQLPreparedStmt* Prepare(lua_State*, std::string query); +}; + +class Lua_MySQLPreparedStmt +{ +public: + Lua_MySQLPreparedStmt(lua_State* L, mysql::PreparedStmt&& stmt) + : m_stmt(std::make_unique(std::move(stmt))) + , m_row_array(luabind::newtable(L)) + , m_row_hash(luabind::newtable(L)) {} + + void Close(); + void Execute(lua_State*); + void Execute(lua_State*, luabind::object args); + void SetOptions(luabind::object table_opts); + luabind::object FetchArray(lua_State*); + luabind::object FetchHash(lua_State*); + + // StmtResult functions accessible through this class to simplify api + int ColumnCount(); + uint64_t LastInsertID(); + uint64_t RowCount(); + uint64_t RowsAffected(); + +private: + std::unique_ptr m_stmt; + mysql::StmtResult m_res = {}; + luabind::object m_row_array; // perf: table cache for fetches + luabind::object m_row_hash; +}; + +#endif // LUA_EQEMU diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index 0b3e4913e..6f624f149 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -42,6 +42,7 @@ #include "lua_spawn.h" #include "lua_spell.h" #include "lua_stat_bonuses.h" +#include "lua_database.h" const char *LuaEvents[_LargestEventID] = { "event_say", @@ -1318,7 +1319,8 @@ void LuaParser::MapFunctions(lua_State *L) { lua_register_expedition(), lua_register_expedition_lock_messages(), lua_register_buff(), - lua_register_exp_source() + lua_register_exp_source(), + lua_register_database() )]; } catch(std::exception &ex) { diff --git a/zone/perl_database.cpp b/zone/perl_database.cpp new file mode 100644 index 000000000..d0e7c1609 --- /dev/null +++ b/zone/perl_database.cpp @@ -0,0 +1,255 @@ +#include "../common/features.h" + +#ifdef EMBPERL_XS_CLASSES + +#include "embperl.h" +#include "perl_database.h" +#include "zonedb.h" + +// Perl takes ownership of returned objects allocated with new and deletes +// them via the DESTROY method when the last perl reference goes out of scope + +void Perl_Database::Destroy(Perl_Database* ptr) +{ + delete ptr; +} + +Perl_Database* Perl_Database::Connect() +{ + return new Perl_Database(); +} + +Perl_Database* Perl_Database::Connect(Connection type) +{ + return new Perl_Database(type); +} + +Perl_Database* Perl_Database::Connect(Connection type, bool connect) +{ + return new Perl_Database(type, connect); +} + +Perl_Database* Perl_Database::Connect(const char* host, const char* user, const char* pass, const char* db, uint32_t port) +{ + return new Perl_Database(host, user, pass, db, port); +} + +Perl_MySQLPreparedStmt* Perl_Database::Prepare(std::string query) +{ + return m_db ? new Perl_MySQLPreparedStmt(m_db->Prepare(std::move(query))) : nullptr; +} + +void Perl_Database::Close() +{ + m_db.reset(); +} + +// --------------------------------------------------------------------------- + +void Perl_MySQLPreparedStmt::Destroy(Perl_MySQLPreparedStmt* ptr) +{ + delete ptr; +} + +void Perl_MySQLPreparedStmt::Close() +{ + m_stmt.reset(); +} + +void Perl_MySQLPreparedStmt::Execute() +{ + if (m_stmt) + { + m_res = m_stmt->Execute(); + } +} + +void Perl_MySQLPreparedStmt::Execute(perl::array args) +{ + // passes all script args as strings + if (m_stmt) + { + std::vector inputs; + for (const perl::scalar& arg : args) + { + if (arg.is_null()) + { + inputs.emplace_back(nullptr); + } + else + { + inputs.emplace_back(arg.c_str()); + } + } + m_res = m_stmt->Execute(inputs); + } +} + +void Perl_MySQLPreparedStmt::SetOptions(perl::hash hash) +{ + if (m_stmt) + { + mysql::StmtOptions opts = m_stmt->GetOptions(); + if (hash.exists("buffer_results")) + { + opts.buffer_results = hash["buffer_results"].as(); + } + if (hash.exists("use_max_length")) + { + opts.use_max_length = hash["use_max_length"].as(); + } + m_stmt->SetOptions(opts); + } +} + +static void PushValue(PerlInterpreter* my_perl, SV* sv, const mysql::StmtColumn& col) +{ + if (col.IsNull()) + { + sv_setsv(sv, &PL_sv_undef); + return; + } + + switch (col.Type()) + { + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_BIT: + if (col.IsUnsigned()) + { + sv_setuv(sv, col.Get().value()); + } + else + { + sv_setiv(sv, col.Get().value()); + } + break; + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + sv_setnv(sv, col.Get().value()); + break; + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + std::string str = col.GetStr().value(); + sv_setpvn(sv, str.data(), str.size()); + } + break; + default: // string types, push raw buffer to avoid copy + { + std::string_view str = col.GetStrView().value(); + sv_setpvn(sv, str.data(), str.size()); + } + break; + } +} + +perl::array Perl_MySQLPreparedStmt::FetchArray() +{ + auto row = m_stmt ? m_stmt->Fetch() : mysql::StmtRow(); + if (!row) + { + return perl::array(); + } + + // perf: bypass perlbind operator[]/push and use cache to limit SV allocs + dTHX; + AV* av = static_cast(m_row_array); + for (const mysql::StmtColumn& col : row) + { + SV** sv = av_fetch(av, col.Index(), true); + PushValue(my_perl, *sv, col); + } + + SvREFCNT_inc(av); // return a ref to our cache (no copy) + return perl::array(std::move(av)); +} + +perl::reference Perl_MySQLPreparedStmt::FetchArrayRef() +{ + perl::array array = FetchArray(); + return array.size() == 0 ? perl::reference() : perl::reference(array); +} + +perl::reference Perl_MySQLPreparedStmt::FetchHashRef() +{ + auto row = m_stmt ? m_stmt->Fetch() : mysql::StmtRow(); + if (!row) + { + return perl::reference(); + } + + // perf: bypass perlbind operator[] and use cache to limit SV allocs + dTHX; + HV* hv = static_cast(m_row_hash); + for (const mysql::StmtColumn& col : row) + { + SV** sv = hv_fetch(hv, col.Name().c_str(), static_cast(col.Name().size()), true); + PushValue(my_perl, *sv, col); + } + + SvREFCNT_inc(hv); // return a ref to our cache (no copy) + return perl::reference(std::move(hv)); +} + +int Perl_MySQLPreparedStmt::ColumnCount() +{ + return m_res.ColumnCount(); +} + +uint64_t Perl_MySQLPreparedStmt::LastInsertID() +{ + return m_res.LastInsertID(); +} + +uint64_t Perl_MySQLPreparedStmt::RowCount() +{ + return m_res.RowCount(); +} + +uint64_t Perl_MySQLPreparedStmt::RowsAffected() +{ + return m_res.RowsAffected(); +} + +void perl_register_database() +{ + perl::interpreter perl(PERL_GET_THX); + + { + auto package = perl.new_class("Database"); + package.add_const("Default", static_cast(QuestDB::Connection::Default)); + package.add_const("Content", static_cast(QuestDB::Connection::Content)); + package.add("DESTROY", &Perl_Database::Destroy); + package.add("new", static_cast(&Perl_Database::Connect)); + package.add("new", static_cast(&Perl_Database::Connect)); + package.add("new", static_cast(&Perl_Database::Connect)); + package.add("new", static_cast(&Perl_Database::Connect)); + package.add("close", &Perl_Database::Close); + package.add("prepare", &Perl_Database::Prepare); + } + + { + auto package = perl.new_class("MySQLPreparedStmt"); + package.add("DESTROY", &Perl_MySQLPreparedStmt::Destroy); + package.add("close", &Perl_MySQLPreparedStmt::Close); + package.add("execute", static_cast(&Perl_MySQLPreparedStmt::Execute)); + package.add("execute", static_cast(&Perl_MySQLPreparedStmt::Execute)); + package.add("fetch", &Perl_MySQLPreparedStmt::FetchArray); + package.add("fetch_array", &Perl_MySQLPreparedStmt::FetchArray); + package.add("fetch_arrayref", &Perl_MySQLPreparedStmt::FetchArrayRef); + package.add("fetch_hashref", &Perl_MySQLPreparedStmt::FetchHashRef); + package.add("insert_id", &Perl_MySQLPreparedStmt::LastInsertID); + package.add("num_fields", &Perl_MySQLPreparedStmt::ColumnCount); + package.add("num_rows", &Perl_MySQLPreparedStmt::RowCount); + package.add("rows_affected", &Perl_MySQLPreparedStmt::RowsAffected); + package.add("set_options", &Perl_MySQLPreparedStmt::SetOptions); + } +} + +#endif // EMBPERL_XS_CLASSES diff --git a/zone/perl_database.h b/zone/perl_database.h new file mode 100644 index 000000000..2c1922bb5 --- /dev/null +++ b/zone/perl_database.h @@ -0,0 +1,50 @@ +#pragma once + +#include "quest_db.h" +#include "../common/mysql_stmt.h" + +class Perl_MySQLPreparedStmt; + +class Perl_Database : public QuestDB +{ +public: + using QuestDB::QuestDB; + + static void Destroy(Perl_Database* ptr); + static Perl_Database* Connect(); + static Perl_Database* Connect(Connection type); + static Perl_Database* Connect(Connection type, bool connect); + static Perl_Database* Connect(const char* host, const char* user, const char* pass, const char* db, uint32_t port); + + void Close(); + Perl_MySQLPreparedStmt* Prepare(std::string query); +}; + +class Perl_MySQLPreparedStmt +{ +public: + Perl_MySQLPreparedStmt(mysql::PreparedStmt&& stmt) + : m_stmt(std::make_unique(std::move(stmt))) {} + + static void Destroy(Perl_MySQLPreparedStmt* ptr); + + void Close(); + void Execute(); + void Execute(perl::array args); + void SetOptions(perl::hash hash_opts); + perl::array FetchArray(); + perl::reference FetchArrayRef(); + perl::reference FetchHashRef(); + + // StmtResult functions accessible through this class to simplify api + int ColumnCount(); + uint64_t LastInsertID(); + uint64_t RowCount(); + uint64_t RowsAffected(); + +private: + std::unique_ptr m_stmt; + mysql::StmtResult m_res = {}; + perl::array m_row_array; // perf: cache for fetches + perl::hash m_row_hash; +}; diff --git a/zone/quest_db.cpp b/zone/quest_db.cpp new file mode 100644 index 000000000..5eda523d4 --- /dev/null +++ b/zone/quest_db.cpp @@ -0,0 +1,57 @@ +#include "quest_db.h" +#include "zonedb.h" +#include "zone_config.h" + +// New connections avoid concurrency issues and allow use of unbuffered results +// with prepared statements. Using zone connections w/o buffering would cause +// "Commands out of sync" errors if any queries occur before results consumed. +QuestDB::QuestDB(Connection type, bool connect) +{ + if (connect) + { + m_db = std::unique_ptr(new Database(), Deleter(true)); + + const auto config = EQEmuConfig::get(); + + if (type == Connection::Default || type == Connection::Content && config->ContentDbHost.empty()) + { + m_db->Connect(config->DatabaseHost, config->DatabaseUsername, config->DatabasePassword, + config->DatabaseDB, config->DatabasePort, "questdb"); + } + else if (type == Connection::Content) + { + m_db->Connect(config->ContentDbHost, config->ContentDbUsername, config->ContentDbPassword, + config->ContentDbName, config->ContentDbPort, "questdb"); + } + } + else if (type == Connection::Default) + { + m_db = std::unique_ptr(&database, Deleter(false)); + } + else if (type == Connection::Content) + { + m_db = std::unique_ptr(&content_db, Deleter(false)); + } + + if (!m_db || (connect && m_db->GetStatus() != DBcore::Connected)) + { + throw std::runtime_error(fmt::format("Failed to connect to db type [{}]", static_cast(type))); + } +} + +QuestDB::QuestDB(const char* host, const char* user, const char* pass, const char* db, uint32_t port) + : m_db(new Database(), Deleter(true)) +{ + if (!m_db->Connect(host, user, pass, db, port, "questdb")) + { + throw std::runtime_error(fmt::format("Failed to connect to db [{}:{}]", host, port)); + } +} + +void QuestDB::Deleter::operator()(Database* ptr) noexcept +{ + if (owner) + { + delete ptr; + } +}; diff --git a/zone/quest_db.h b/zone/quest_db.h new file mode 100644 index 000000000..c1d897cc4 --- /dev/null +++ b/zone/quest_db.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +class Database; + +// Base class for quest apis to manage connection to a MySQL database +class QuestDB +{ +public: + enum class Connection { Default = 0, Content }; + + // Throws std::runtime_error on connection failure + QuestDB() : QuestDB(Connection::Default) {} + QuestDB(Connection type) : QuestDB(type, false) {} + QuestDB(Connection type, bool connect); + QuestDB(const char* host, const char* user, const char* pass, const char* db, uint32_t port); + +protected: + // allow optional ownership of pointer to support using zone db connections + struct Deleter + { + Deleter() : owner(true) {} + Deleter(bool owner_) : owner(owner_) {} + bool owner = true; + void operator()(Database* ptr) noexcept; + }; + + std::unique_ptr m_db; +}; From e4d812f4b4e2267e06b7c7632fd4ae3767c84bd5 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Wed, 13 Nov 2024 20:08:03 -0600 Subject: [PATCH 22/26] [Release] 22.59.0 (#4538) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ common/version.h | 2 +- package.json | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90dfe1041..c5ccdb119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [22.59.0] 11/13/2024 + +### Databuckets + +* Add database index to data_buckets ([#4535](https://github.com/EQEmu/Server/pull/4535)) @Akkadius 2024-11-09 + +### Fixes + +* Bazaar two edge case issues resolved ([#4533](https://github.com/EQEmu/Server/pull/4533)) @neckkola 2024-11-09 +* Check if the mob is already in the close mobs list before inserting @Akkadius 2024-11-11 +* ScanCloseMobs - Ensure scanning mob has an entity ID @Akkadius 2024-11-10 + +### Performance + +* Improvements to ScanCloseMobs logic ([#4534](https://github.com/EQEmu/Server/pull/4534)) @Akkadius 2024-11-08 + +### Quest API + +* Add Native Database Querying Interface ([#4531](https://github.com/EQEmu/Server/pull/4531)) @hgtw 2024-11-13 + +### Rules + +* Add Rule for restricting client versions to world server ([#4527](https://github.com/EQEmu/Server/pull/4527)) @knervous 2024-11-12 + ## [22.58.0] 11/5/2024 ### Code diff --git a/common/version.h b/common/version.h index 708dbe9d2..dcc7d8bf8 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.58.0-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.59.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 8ff6f1581..03c09a9dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.58.0", + "version": "22.59.0", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" From e4aa6a6957154336cdbf88bec2a2be4e23d4ce8e Mon Sep 17 00:00:00 2001 From: Akkadius Date: Wed, 13 Nov 2024 20:52:46 -0600 Subject: [PATCH 23/26] [Release] 22.59.1 --- CHANGELOG.md | 6 ++++++ common/database/database_update_manifest.cpp | 2 +- common/version.h | 2 +- package.json | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ccdb119..14e5cd22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [22.59.1] 11/13/2024 + +### Hotfix + +* Fix faulty database migration condition with databuckets (9285) + ## [22.59.0] 11/13/2024 ### Databuckets diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index e09a24664..636899094 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5764,7 +5764,7 @@ MODIFY COLUMN `exp_modifier` float NOT NULL DEFAULT 1.0 AFTER `aa_modifier`; .version = 9285, .description = "2024_11_08_data_buckets_indexes.sql", .check = "SHOW CREATE TABLE `data_buckets`", - .condition = "contains", + .condition = "missing", .match = "idx_character_expires", .sql = R"( CREATE INDEX idx_character_expires ON data_buckets (character_id, expires); diff --git a/common/version.h b/common/version.h index dcc7d8bf8..c615c7c85 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.0-dev" // always append -dev to the current version for custom-builds +#define CURRENT_VERSION "22.59.1-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 03c09a9dd..f2590babb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.59.0", + "version": "22.59.1", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" From 33f5c4c6a750f74c6b557b61a8b191ab3c8ae1cf Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 14 Nov 2024 18:15:03 -0600 Subject: [PATCH 24/26] [Bug Fix] Fix issue where NPC's are being hidden as traders (#4539) * [Fix] Fix issue where NPC's are being hidden as traders * Fix * Update mob.cpp --- zone/mob.cpp | 2 -- zone/npc.cpp | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/zone/mob.cpp b/zone/mob.cpp index 2b43ff5e2..a06dbc743 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -1266,8 +1266,6 @@ void Mob::CreateSpawnPacket(EQApplicationPacket* app, NewSpawn_Struct* ns) { } else { strcpy(ns2->spawn.lastName, ns->spawn.lastName); } - - memset(&app->pBuffer[sizeof(Spawn_Struct)-7], 0xFF, 7); } void Mob::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) diff --git a/zone/npc.cpp b/zone/npc.cpp index f65bd7fad..f1260cd9c 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -2156,6 +2156,7 @@ void NPC::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) UpdateActiveLight(); ns->spawn.light = GetActiveLightType(); ns->spawn.show_name = NPCTypedata->show_name; + ns->spawn.trader = false; } void NPC::PetOnSpawn(NewSpawn_Struct* ns) From d3da2e5501f27889907fe12404bb3c0bde48d0e8 Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:32:19 -0400 Subject: [PATCH 25/26] [Fix] Fix for bazaar search of containers. (#4540) --- common/bazaar.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/bazaar.cpp b/common/bazaar.cpp index 47b2a7dc8..789182821 100644 --- a/common/bazaar.cpp +++ b/common/bazaar.cpp @@ -235,7 +235,8 @@ Bazaar::GetSearchResults( std::vector item_search_types = { {EQ::item::ItemType::ItemTypeAll, true}, {EQ::item::ItemType::ItemTypeBook, item->ItemClass == EQ::item::ItemType::ItemTypeBook}, - {EQ::item::ItemType::ItemTypeContainer, item->ItemClass == EQ::item::ItemType::ItemTypeContainer}, + {EQ::item::ItemType::ItemTypeContainer, item->ItemClass == EQ::item::ItemType::ItemTypeContainer || + item->IsClassBag()}, {EQ::item::ItemType::ItemTypeAllEffects, item->Scroll.Effect > 0 && item->Scroll.Effect < 65000}, {EQ::item::ItemType::ItemTypeUnknown9, item->Worn.Effect == 998}, {EQ::item::ItemType::ItemTypeUnknown10, item->Worn.Effect >= 1298 && item->Worn.Effect <= 1307}, From 9967384ab8831e322778b71f8583c5aa96fdcbbf Mon Sep 17 00:00:00 2001 From: Mitch Freeman <65987027+neckkola@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:44:03 -0400 Subject: [PATCH 26/26] [Fix] Fix for mult-instanced bazaar zones (#4541) * Enable bazaar for multiple instances. * Enable buyer for multiple instances. * Update to buyer/barter for multiple instances and attuned items. --- common/eq_packet_structs.h | 1 + world/zoneserver.cpp | 18 +++++++++++++++--- zone/trading.cpp | 9 +++++++++ zone/worldserver.cpp | 1 + 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index e9b80aaf0..322bdf639 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -3221,6 +3221,7 @@ struct BuyerMessaging_Struct { char item_name[64]; uint32 slot; uint32 seller_quantity; + uint32 purchase_method; // 0 direct merchant, 1 via /barter window }; struct BuyerAddBuyertoBarterWindow_Struct { diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index c54164594..b00afb4b7 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1755,7 +1755,11 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { return; } - zoneserver_list.SendPacket(Zones::BAZAAR, pack); + auto trader = client_list.FindCLEByCharacterID(in->trader_buy_struct.trader_id); + if (trader) { + zoneserver_list.SendPacket(trader->zone(), trader->instance(), pack); + } + break; } case ServerOP_BuyerMessaging: { @@ -1775,12 +1779,20 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { break; } case Barter_SellItem: { - zoneserver_list.SendPacket(Zones::BAZAAR, pack); + auto buyer = client_list.FindCharacter(in->buyer_name); + if (buyer) { + zoneserver_list.SendPacket(buyer->zone(), buyer->instance(), pack); + } + break; } case Barter_FailedTransaction: case Barter_BuyerTransactionComplete: { - zoneserver_list.SendPacket(in->zone_id, pack); + auto seller = client_list.FindCharacter(in->seller_name); + if (seller) { + zoneserver_list.SendPacket(seller->zone(), seller->instance(), pack); + } + break; } default: diff --git a/zone/trading.cpp b/zone/trading.cpp index ac142319e..7a8abc2e9 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -2606,6 +2606,7 @@ void Client::SellToBuyer(const EQApplicationPacket *app) data->zone_id = GetZoneID(); data->slot = sell_line.slot; data->seller_quantity = sell_line.seller_quantity; + data->purchase_method = sell_line.purchase_method; strn0cpy(data->item_name, sell_line.item_name, sizeof(data->item_name)); strn0cpy(data->buyer_name, sell_line.buyer_name.c_str(), sizeof(data->buyer_name)); strn0cpy(data->seller_name, GetCleanName(), sizeof(data->seller_name)); @@ -4252,6 +4253,14 @@ bool Client::DoBarterSellerChecks(BuyerLineSellItem_Struct &sell_line) Message(Chat::Red, "The item that you are trying to sell is augmented. Please remove augments first"); } + if (sell_item && !sell_item->IsDroppable()) { + seller_error = true; + LogTradingDetail("Seller item [{}] is non-tradeable therefore cannot be sold.", + sell_line.item_name + ); + Message(Chat::Red, "The item that you are trying to sell is non-tradeable and therefore cannot be sold."); + } + if (seller_error) { LogTradingDetail("Seller Error [{}] Barter Sell/Buy Transaction Failed.", seller_error); SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 76e24c962..acfb686df 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -4044,6 +4044,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) sell_line.buyer_name = in->buyer_name; sell_line.seller_quantity = in->seller_quantity; sell_line.slot = in->slot; + sell_line.purchase_method = in->purchase_method; strn0cpy(sell_line.item_name, in->item_name, sizeof(sell_line.item_name)); uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity;