diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7cc690a..14e5cd22c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,123 @@ +## [22.59.1] 11/13/2024 + +### Hotfix + +* Fix faulty database migration condition with databuckets (9285) + +## [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 + +* 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 + +* 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 + +* 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/CMakeLists.txt b/common/CMakeLists.txt index dcc778bf2..3bbaecdb4 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 @@ -588,6 +589,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/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}, diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 63f3ba35b..636899094 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 = "missing", + .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/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/eq_packet_structs.h b/common/eq_packet_structs.h index b626e94a0..8c091ebea 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/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/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 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/common/ruletypes.h b/common/ruletypes.h index 77a1ee7f7..ec6f50dc3 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_INT(World, Id, 100, "Used by later clients to create GUIDs, expected to be Unique to the world but ultimately not that important") RULE_CATEGORY_END() @@ -519,6 +520,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/common/version.h b/common/version.h index 91d4c7494..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.56.3-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__ @@ -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 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() 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}]", diff --git a/package.json b/package.json index c73e7c47a..f2590babb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eqemu-server", - "version": "22.56.3", + "version": "22.59.1", "repository": { "type": "git", "url": "https://github.com/EQEmu/Server.git" 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); 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/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/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/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/bot_database.cpp b/zone/bot_database.cpp index 3a07b2649..a24cd0c30 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; @@ -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; } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index c880e310f..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); @@ -5013,7 +5021,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/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) || 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/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/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/entity.cpp b/zone/entity.cpp index c21856d37..4a9a232ca 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2945,8 +2945,22 @@ 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. + // 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 +2971,17 @@ 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 + // 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; } } - 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/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_general.cpp b/zone/lua_general.cpp index 74ed3a721..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 */ @@ -6583,7 +6595,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), @@ -6772,7 +6784,6 @@ luabind::scope lua_register_random() { )]; } - luabind::scope lua_register_events() { return luabind::class_("Event") .enum_("constants") @@ -8008,7 +8019,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..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", @@ -1312,11 +1313,14 @@ 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(), + lua_register_database() )]; } catch(std::exception &ex) { diff --git a/zone/mob.cpp b/zone/mob.cpp index a1d9b5d38..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) @@ -8584,6 +8582,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 +8592,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..f1260cd9c 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()) { @@ -2151,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) 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; +}; 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; 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; } diff --git a/zone/trading.cpp b/zone/trading.cpp index ac36f014f..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)); @@ -3497,7 +3498,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( @@ -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; 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; } }