Compare commits

..

24 Commits

Author SHA1 Message Date
Alex King 3d7cf4235c [Release] 22.60.0 (#4555) 2024-11-25 17:17:03 -06:00
hg 187ee10218 [Tasks] Update tasks in all zones if invalid zone set (#4550)
This allows task elements to update in any zone when it has an invalid
zone id <= 0. This has the same effect as leaving the zones field empty
except "Unknown Zone" instead of "ALL" will be shown in task windows.

Note that Titanium shows "ALL" for a zone id of 0 despite it being an
invalid zone id. This could be manipulated server side to match newer
clients but there isn't much benefit since any other invalid zone id
below 0 can be used to do the same.
2024-11-25 18:02:14 -05:00
Alex King 6bd758b3dd [Rules] Add Rule to Disable NPCs Facing Target (#4543) 2024-11-24 17:30:44 -06:00
Alex King 9938755517 [Bug Fix] Fix Possible Item Loss in Trades (#4554) 2024-11-24 17:29:27 -06:00
Chris Miles 630da0eee6 [Config] Fix World TCP Address Configuration Default (#4551) 2024-11-24 17:27:31 -06:00
Alex King 3158386aa3 [Bug Fix] Fix Issue with Perl EVENT_PAYLOAD (#4545) 2024-11-24 17:19:40 -06:00
hg 12ada57ee8 [Code] Fix build with older C++ libraries (#4549)
This adds a compile time concept to determine if from_chars has
floating-point support and uses fallbacks if not.

This is a C++17 feature but support for floats was only added to
libstdc++ with GCC 11.1 and LLVM libc++ in 20.0 (unreleased).
2024-11-24 17:18:36 -06:00
Mitch Freeman 7a841c11c5 [Bug Fix] Players could become flagged as a Trader when they were not trading (#4553) 2024-11-24 17:17:01 -06:00
Mitch Freeman a49d1446b7 [Bug Fix] Fix for sending money via Parcel, then changing your mind (#4552) 2024-11-24 17:15:18 -06:00
carolus21rex b2d0fa6a2f [Bug Fix] Fix Strings::Commify bug with #mystats (#4547)
* Fix a formatting bug with #mystats

When using values larger than 1,000, we were calling commify on a string that already had commas. This resulted in the value 1005 looking like 1,,005.

* Update mob.cpp

---------

Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com>
2024-11-22 16:23:48 -05:00
Mitch Freeman 62ac015fff [Bug Fix] Fix an edge case with augmented items inside parceled containers (#4546) 2024-11-20 21:17:04 -05:00
Mitch Freeman 4977a7c2e0 [Bazaar] Further refinements for instanced bazaar (#4544)
Resolves
- Parcels being delivered with incorrect item
- Inspecting an item from the bazaar window showing the incorrect item
2024-11-16 15:14:17 -06:00
Mitch Freeman 9967384ab8 [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.
2024-11-14 19:44:03 -06:00
Mitch Freeman d3da2e5501 [Fix] Fix for bazaar search of containers. (#4540) 2024-11-14 19:32:19 -06:00
Chris Miles 33f5c4c6a7 [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
2024-11-14 19:15:03 -05:00
Akkadius e4aa6a6957 [Release] 22.59.1 2024-11-13 20:52:46 -06:00
Chris Miles e4d812f4b4 [Release] 22.59.0 (#4538) 2024-11-13 20:08:03 -06:00
hg bcedfe7032 [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
2024-11-12 20:01:18 -06:00
Paul Johnson c1df3fbcb0 [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 <Paul@PJOHNSOMAC-6366.digi.box>
2024-11-12 11:00:22 -05:00
Akkadius 011e1d05e7 [Hotfix] Check if the mob is already in the close mobs list before inserting 2024-11-10 23:19:40 -06:00
Akkadius 3f0f95976c [Hotfix] ScanCloseMobs - Ensure scanning mob has an entity ID 2024-11-10 06:47:42 -06:00
Chris Miles 77de9619b5 [Databuckets] Add database index to data_buckets (#4535)
* [Databuckets] Add database index to data_buckets

* Update database_update_manifest.cpp
2024-11-08 22:26:00 -05:00
Mitch Freeman 20d3ab2ac5 [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.
2024-11-08 22:15:12 -05:00
Chris Miles 0ea47fadee [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.
2024-11-08 17:48:39 -06:00
36 changed files with 1010 additions and 117 deletions
+64
View File
@@ -1,3 +1,67 @@
## [22.60.0] 11/25/2024
### Bazaar
* Further refinements for instanced bazaar ([#4544](https://github.com/EQEmu/Server/pull/4544)) @neckkola 2024-11-16
### Code
* Fix build with older C++ libraries ([#4549](https://github.com/EQEmu/Server/pull/4549)) @hgtw 2024-11-24
### Config
* Fix World TCP Address Configuration Default ([#4551](https://github.com/EQEmu/Server/pull/4551)) @Akkadius 2024-11-24
### Fixes
* Fix Issue with Perl EVENT_PAYLOAD ([#4545](https://github.com/EQEmu/Server/pull/4545)) @Kinglykrab 2024-11-24
* Fix Possible Item Loss in Trades ([#4554](https://github.com/EQEmu/Server/pull/4554)) @Kinglykrab 2024-11-24
* Fix Strings::Commify bug with #mystats ([#4547](https://github.com/EQEmu/Server/pull/4547)) @carolus21rex 2024-11-22
* Fix an edge case with augmented items inside parceled containers ([#4546](https://github.com/EQEmu/Server/pull/4546)) @neckkola 2024-11-21
* Fix for bazaar search of containers. ([#4540](https://github.com/EQEmu/Server/pull/4540)) @neckkola 2024-11-15
* Fix for mult-instanced bazaar zones ([#4541](https://github.com/EQEmu/Server/pull/4541)) @neckkola 2024-11-15
* Fix for sending money via Parcel, then changing your mind ([#4552](https://github.com/EQEmu/Server/pull/4552)) @neckkola 2024-11-24
* Fix issue where NPC's are being hidden as traders ([#4539](https://github.com/EQEmu/Server/pull/4539)) @Akkadius 2024-11-15
* Players could become flagged as a Trader when they were not trading ([#4553](https://github.com/EQEmu/Server/pull/4553)) @neckkola 2024-11-24
### Rules
* Add Rule to Disable NPCs Facing Target ([#4543](https://github.com/EQEmu/Server/pull/4543)) @Kinglykrab 2024-11-24
### Tasks
* Update tasks in all zones if invalid zone set ([#4550](https://github.com/EQEmu/Server/pull/4550)) @hgtw 2024-11-25
## [22.59.1] 11/13/2024
### Hotfix
* 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
+2 -1
View File
@@ -235,7 +235,8 @@ Bazaar::GetSearchResults(
std::vector<ItemSearchType> 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},
@@ -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
+1
View File
@@ -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 {
+1 -1
View File
@@ -94,7 +94,7 @@ void EQEmuConfig::parse_config()
auto_database_updates = true;
}
WorldIP = _root["server"]["world"]["tcp"].get("host", "127.0.0.1").asString();
WorldIP = _root["server"]["world"]["tcp"].get("ip", "127.0.0.1").asString();
WorldTCPPort = Strings::ToUnsignedInt(_root["server"]["world"]["tcp"].get("port", "9000").asString());
TelnetIP = _root["server"]["world"]["telnet"].get("ip", "127.0.0.1").asString();
+14
View File
@@ -414,6 +414,12 @@ static uint64_t MakeBits(std::span<const uint8_t> data)
return bits;
}
template <typename T>
concept has_from_chars = requires (const char* first, const char* last, T value)
{
std::from_chars(first, last, value);
};
template <typename T>
static T FromString(std::string_view sv)
{
@@ -422,6 +428,14 @@ static T FromString(std::string_view sv)
// return false for empty (zero-length) strings
return !sv.empty();
}
else if constexpr (std::is_same_v<T, float> && !has_from_chars<T>)
{
return std::strtof(std::string(sv).c_str(), nullptr);
}
else if constexpr (std::is_same_v<T, double> && !has_from_chars<T>)
{
return std::strtod(std::string(sv).c_str(), nullptr);
}
else
{
// non numbers return a zero initialized T (could return nullopt instead)
+8 -10
View File
@@ -164,37 +164,35 @@ public:
return UpdateOne(db, m);
}
static Trader GetItemBySerialNumber(Database &db, uint32 serial_number)
static Trader GetItemBySerialNumber(Database &db, uint32 serial_number, uint32 trader_id)
{
Trader e{};
const auto trader_item = GetWhere(
db,
fmt::format("`item_sn` = '{}' LIMIT 1", serial_number)
fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, serial_number)
);
if (trader_item.empty()) {
return e;
}
else {
return trader_item.at(0);
}
return trader_item.at(0);
}
static Trader GetItemBySerialNumber(Database &db, std::string serial_number)
static Trader GetItemBySerialNumber(Database &db, std::string serial_number, uint32 trader_id)
{
Trader e{};
auto sn = Strings::ToUnsignedBigInt(serial_number);
const auto trader_item = GetWhere(
db,
fmt::format("`item_sn` = '{}' LIMIT 1", sn)
fmt::format("`char_id` = '{}' AND `item_sn` = '{}' LIMIT 1", trader_id, sn)
);
if (trader_item.empty()) {
return e;
}
else {
return trader_item.at(0);
}
return trader_item.at(0);
}
static int UpdateActiveTransaction(Database &db, uint32 id, bool status)
+2
View File
@@ -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)
@@ -680,6 +681,7 @@ RULE_BOOL(NPC, DisableLastNames, false, "Enable to disable NPC Last Names")
RULE_BOOL(NPC, NPCIgnoreLevelBasedHasteCaps, false, "Ignores hard coded level based haste caps.")
RULE_INT(NPC, NPCHasteCap, 150, "Haste cap for non-v3(over haste) haste")
RULE_INT(NPC, NPCHastev3Cap, 25, "Haste cap for v3(over haste) haste")
RULE_STRING(NPC, ExcludedFaceTargetRaces, "52,72,73,141,233,328,329,372,376,377,378,379,380,381,382,383,404,422,423,424,425,426,428,429,445,449,460,462,463,500,501,502,503,504,505,506,507,508,509,510,511,513,514,515,516,533,534,535,536,537,538,539,540,541,542,543,544,545,546,550,551,552,553,554,555,556,557,567,573,577,586,589,590,591,592,593,595,596,599,601,616,619,621,628,629,630,633,634,635,636,665,683,684,685,691,692,693,694,702,703,705,706,707,710,711,714,720,2250,2254", "Race IDs excluded from facing target when hailed")
RULE_CATEGORY_END()
RULE_CATEGORY(Aggro)
+1
View File
@@ -1945,6 +1945,7 @@ struct ServerOP_GuildMessage_Struct {
struct TraderMessaging_Struct {
uint32 action;
uint32 zone_id;
uint32 instance_id;
uint32 trader_id;
uint32 entity_id;
char trader_name[64];
+4 -3
View File
@@ -83,7 +83,8 @@ struct ActivityInformation {
if (zone_ids.empty()) {
return true;
}
bool found_zone = std::find(zone_ids.begin(), zone_ids.end(), zone_id) != zone_ids.end();
bool found_zone = std::any_of(zone_ids.begin(), zone_ids.end(),
[zone_id](int id) { return id <= 0 || id == zone_id; });
return found_zone && (zone_version == version || zone_version == -1);
}
@@ -100,7 +101,7 @@ struct ActivityInformation {
out.WriteInt32(activity_type == TaskActivityType::GiveCash ? 1 : goal_count);
out.WriteLengthString(skill_list); // used in SkillOn objective type string, "-1" for none
out.WriteLengthString(spell_list); // used in CastOn objective type string, "0" for none
out.WriteString(zones); // used in objective zone column and task select "begins in" (may have multiple, "0" for "unknown zone", empty for "ALL")
out.WriteString(zones); // used in ui zone columns and task select "begins in" (may have multiple, invalid id for "Unknown Zone", empty for "ALL")
}
else
{
@@ -114,7 +115,7 @@ struct ActivityInformation {
out.WriteString(description_override);
if (client_version >= EQ::versions::ClientVersion::RoF) {
out.WriteString(zones); // serialized again after description (seems unused)
out.WriteString(zones); // target zone version internal id (unused client side)
}
}
+2 -2
View File
@@ -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.60.0-dev" // always append -dev to the current version for custom-builds
#define LOGIN_VERSION "0.8.0"
#define COMPILE_DATE __DATE__
#define COMPILE_TIME __TIME__
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eqemu-server",
"version": "22.58.0",
"version": "22.60.0",
"repository": {
"type": "git",
"url": "https://github.com/EQEmu/Server.git"
+52 -3
View File
@@ -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<uint16>::max();
e->Instance = 0;
e->Gender = Gender::Male;
e->GoHome = 0;
e->Tutorial = 0;
e->Enabled = 0;
QueuePacket(&packet);
}
+1
View File
@@ -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);
+15 -3
View File
@@ -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:
+6
View File
@@ -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
+4 -1
View File
@@ -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()) {
+13 -1
View File
@@ -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;
+3 -1
View File
@@ -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
+3 -1
View File
@@ -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
}
@@ -1734,7 +1736,7 @@ void PerlembParser::ExportEventVariables(
case EVENT_PAYLOAD: {
Seperator sep(data);
ExportVar(package_name.c_str(), "payload_id", sep.arg[0]);
ExportVar(package_name.c_str(), "payload_value", sep.arg[1]);
ExportVar(package_name.c_str(), "payload_value", sep.argplus[1]);
break;
}
+2
View File
@@ -21,6 +21,8 @@ Eglin
#include <perlbind/perlbind.h>
namespace perl = perlbind;
#undef connect
#undef bind
#undef Null
#ifdef WIN32
+21 -18
View File
@@ -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<uint16, Mob *>(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<uint16, Mob *>(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"
+214
View File
@@ -0,0 +1,214 @@
#ifdef LUA_EQEMU
#include "lua_database.h"
#include "zonedb.h"
#include <luabind/luabind.hpp>
#include <luabind/adopt_policy.hpp>
// 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<mysql::PreparedStmt::param_t> 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<bool>(args[i]));
break;
case LUA_TNUMBER: // all numbers are doubles in lua before 5.3
inputs.emplace_back(luabind::object_cast<lua_Number>(args[i]));
break;
case LUA_TSTRING:
inputs.emplace_back(luabind::object_cast<const char*>(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<bool>(table["buffer_results"]);
}
if (luabind::type(table["use_max_length"]) == LUA_TBOOLEAN)
{
opts.use_max_length = luabind::object_cast<bool>(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<lua_Number>().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_<Lua_Database>("Database")
.enum_("constants")
[(
luabind::value("Default", static_cast<int>(QuestDB::Connection::Default)),
luabind::value("Content", static_cast<int>(QuestDB::Connection::Content))
)]
.def(luabind::constructor<>())
.def(luabind::constructor<QuestDB::Connection>())
.def(luabind::constructor<QuestDB::Connection, bool>())
.def(luabind::constructor<const char*, const char*, const char*, const char*, uint32_t>())
.def("close", &Lua_Database::Close)
.def("prepare", &Lua_Database::Prepare, luabind::adopt(luabind::result)),
luabind::class_<Lua_MySQLPreparedStmt>("MySQLPreparedStmt")
.def("close", &Lua_MySQLPreparedStmt::Close)
.def("execute", static_cast<void(Lua_MySQLPreparedStmt::*)(lua_State*)>(&Lua_MySQLPreparedStmt::Execute))
.def("execute", static_cast<void(Lua_MySQLPreparedStmt::*)(lua_State*, luabind::object)>(&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
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#ifdef LUA_EQEMU
#include "quest_db.h"
#include "../common/mysql_stmt.h"
#include <luabind/object.hpp>
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<mysql::PreparedStmt>(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<mysql::PreparedStmt> m_stmt;
mysql::StmtResult m_res = {};
luabind::object m_row_array; // perf: table cache for fetches
luabind::object m_row_hash;
};
#endif // LUA_EQEMU
+3 -1
View File
@@ -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) {
+36 -48
View File
@@ -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)
@@ -2050,19 +2048,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window)
case 0: {
mod2a_name = "Avoidance";
mod2b_name = "Combat Effects";
mod2a_cap = Strings::Commify(RuleI(Character, ItemAvoidanceCap));
mod2b_cap = Strings::Commify(RuleI(Character, ItemCombatEffectsCap));
mod2a_cap = RuleI(Character, ItemAvoidanceCap);
mod2b_cap = RuleI(Character, ItemCombatEffectsCap);
if (IsBot()) {
mod2a = Strings::Commify(CastToBot()->GetAvoidance());
mod2a = CastToBot()->GetAvoidance();
} else if (IsClient()) {
mod2a = Strings::Commify(CastToClient()->GetAvoidance());
mod2a = CastToClient()->GetAvoidance();
}
if (IsBot()) {
mod2b = Strings::Commify(CastToBot()->GetCombatEffects());
mod2b = CastToBot()->GetCombatEffects();
} else if (IsClient()) {
mod2b = Strings::Commify(CastToClient()->GetCombatEffects());
mod2b = CastToClient()->GetCombatEffects();
}
break;
@@ -2070,19 +2068,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window)
case 1: {
mod2a_name = "Accuracy";
mod2b_name = "Strikethrough";
mod2a_cap = Strings::Commify(RuleI(Character, ItemAccuracyCap));
mod2b_cap = Strings::Commify(RuleI(Character, ItemStrikethroughCap));
mod2a_cap = RuleI(Character, ItemAccuracyCap);
mod2b_cap = RuleI(Character, ItemStrikethroughCap);
if (IsBot()) {
mod2a = Strings::Commify(CastToBot()->GetAccuracy());
mod2a = CastToBot()->GetAccuracy();
} else if (IsClient()) {
mod2a = Strings::Commify(CastToClient()->GetAccuracy());
mod2a = CastToClient()->GetAccuracy();
}
if (IsBot()) {
mod2b = Strings::Commify(CastToBot()->GetStrikeThrough());
mod2b = CastToBot()->GetStrikeThrough();
} else if (IsClient()) {
mod2b = Strings::Commify(CastToClient()->GetStrikeThrough());
mod2b = CastToClient()->GetStrikeThrough();
}
break;
@@ -2090,20 +2088,20 @@ void Mob::SendStatsWindow(Client* c, bool use_window)
case 2: {
mod2a_name = "Shielding";
mod2b_name = "Spell Shielding";
mod2a_cap = Strings::Commify(RuleI(Character, ItemShieldingCap));
mod2b_cap = Strings::Commify(RuleI(Character, ItemSpellShieldingCap));
mod2a_cap = RuleI(Character, ItemShieldingCap);
mod2b_cap = RuleI(Character, ItemSpellShieldingCap);
if (IsBot()) {
mod2a = Strings::Commify(CastToBot()->GetShielding());
mod2a = CastToBot()->GetShielding();
} else if (IsClient()) {
mod2a = Strings::Commify(CastToClient()->GetShielding());
mod2a = CastToClient()->GetShielding();
}
if (IsBot()) {
mod2b = Strings::Commify(CastToBot()->GetSpellShield());
mod2b = CastToBot()->GetSpellShield();
} else if (IsClient()) {
mod2b = Strings::Commify(CastToClient()->GetSpellShield());
mod2b = CastToClient()->GetSpellShield();
}
break;
@@ -2111,19 +2109,19 @@ void Mob::SendStatsWindow(Client* c, bool use_window)
case 3: {
mod2a_name = "Stun Resist";
mod2b_name = "DOT Shielding";
mod2a_cap = Strings::Commify(RuleI(Character, ItemStunResistCap));
mod2b_cap = Strings::Commify(RuleI(Character, ItemDoTShieldingCap));
mod2a_cap = RuleI(Character, ItemStunResistCap);
mod2b_cap = RuleI(Character, ItemDoTShieldingCap);
if (IsBot()) {
mod2a = Strings::Commify(CastToBot()->GetStunResist());
mod2a = CastToBot()->GetStunResist();
} else if (IsClient()) {
mod2a = Strings::Commify(CastToClient()->GetStunResist());
mod2a = CastToClient()->GetStunResist();
}
if (IsBot()) {
mod2b = Strings::Commify(CastToBot()->GetDoTShield());
mod2b = CastToBot()->GetDoTShield();
} else if (IsClient()) {
mod2b = Strings::Commify(CastToClient()->GetDoTShield());
mod2b = CastToClient()->GetDoTShield();
}
break;
@@ -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);
}
}
-1
View File
@@ -1488,7 +1488,6 @@ public:
bool IsCloseToBanker();
void ScanCloseMobProcess();
std::unordered_map<uint16, Mob *> &GetCloseMobList(float distance = 0.0f);
void CheckScanCloseMobsMovingTimer();
+42 -10
View File
@@ -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)
@@ -3291,16 +3297,28 @@ uint32 NPC::GetSpawnKillCount()
return(0);
}
void NPC::DoQuestPause(Mob *other) {
if(IsMoving() && !IsOnHatelist(other)) {
PauseWandering(RuleI(NPC, SayPauseTimeInSec));
if (other && !other->sneaking)
FaceTarget(other);
} else if(!IsMoving()) {
if (other && !other->sneaking && GetAppearance() != eaSitting && GetAppearance() != eaDead)
FaceTarget(other);
void NPC::DoQuestPause(Mob* m)
{
if (!m) {
return;
}
if (IsMoving() && !IsOnHatelist(m)) {
PauseWandering(RuleI(NPC, SayPauseTimeInSec));
if (FacesTarget() && !m->sneaking) {
FaceTarget(m);
}
} else if (!IsMoving()) {
if (
FacesTarget() &&
!m->sneaking &&
GetAppearance() != eaSitting &&
GetAppearance() != eaDead
) {
FaceTarget(m);
}
}
}
void NPC::ChangeLastName(std::string last_name)
@@ -4232,3 +4250,17 @@ void NPC::DoNpcToNpcAggroScan()
false
);
}
bool NPC::FacesTarget()
{
const std::string& excluded_races_rule = RuleS(NPC, ExcludedFaceTargetRaces);
if (excluded_races_rule.empty()) {
return true;
}
const auto& v = Strings::Split(excluded_races_rule, ",");
return std::find(v.begin(), v.end(), std::to_string(GetBaseRace())) == v.end();
}
+2 -1
View File
@@ -482,7 +482,8 @@ public:
NPC_Emote_Struct* GetNPCEmote(uint32 emote_id, uint8 event_);
void DoNPCEmote(uint8 event_, uint32 emote_id, Mob* t = nullptr);
bool CanTalk();
void DoQuestPause(Mob *other);
void DoQuestPause(Mob* m);
bool FacesTarget();
inline void SetSpellScale(float amt) { spellscale = amt; }
inline float GetSpellScale() { return spellscale; }
+14 -2
View File
@@ -278,6 +278,19 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in)
return;
}
if (parcel_in->money_flag && parcel_in->item_slot != INVALID_INDEX) {
Message(
Chat::Yellow,
fmt::format(
"{} tells you, 'I am confused! Do you want to send money or an item?'",
merchant->GetCleanName()
).c_str()
);
DoParcelCancel();
SendParcelAck();
return;
}
auto num_of_parcels = GetParcelCount();
if (num_of_parcels >= RuleI(Parcel, ParcelMaxItems)) {
SendParcelIconStatus();
@@ -406,9 +419,8 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in)
std::vector<CharacterParcelsContainersRepository::CharacterParcelsContainers> all_entries{};
if (inst->IsNoneEmptyContainer()) {
CharacterParcelsContainersRepository::CharacterParcelsContainers cpc{};
for (auto const &kv: *inst->GetContents()) {
CharacterParcelsContainersRepository::CharacterParcelsContainers cpc{};
cpc.parcels_id = result.id;
cpc.slot_id = kv.first;
cpc.item_id = kv.second->GetID();
+255
View File
@@ -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<mysql::PreparedStmt::param_t> 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<bool>();
}
if (hash.exists("use_max_length"))
{
opts.use_max_length = hash["use_max_length"].as<bool>();
}
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<UV>().value());
}
else
{
sv_setiv(sv, col.Get<IV>().value());
}
break;
case MYSQL_TYPE_FLOAT:
case MYSQL_TYPE_DOUBLE:
sv_setnv(sv, col.Get<NV>().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<AV*>(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<HV*>(m_row_hash);
for (const mysql::StmtColumn& col : row)
{
SV** sv = hv_fetch(hv, col.Name().c_str(), static_cast<I32>(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<Perl_Database>("Database");
package.add_const("Default", static_cast<int>(QuestDB::Connection::Default));
package.add_const("Content", static_cast<int>(QuestDB::Connection::Content));
package.add("DESTROY", &Perl_Database::Destroy);
package.add("new", static_cast<Perl_Database*(*)()>(&Perl_Database::Connect));
package.add("new", static_cast<Perl_Database*(*)(QuestDB::Connection)>(&Perl_Database::Connect));
package.add("new", static_cast<Perl_Database*(*)(QuestDB::Connection, bool)>(&Perl_Database::Connect));
package.add("new", static_cast<Perl_Database*(*)(const char*, const char*, const char*, const char*, uint32_t)>(&Perl_Database::Connect));
package.add("close", &Perl_Database::Close);
package.add("prepare", &Perl_Database::Prepare);
}
{
auto package = perl.new_class<Perl_MySQLPreparedStmt>("MySQLPreparedStmt");
package.add("DESTROY", &Perl_MySQLPreparedStmt::Destroy);
package.add("close", &Perl_MySQLPreparedStmt::Close);
package.add("execute", static_cast<void(Perl_MySQLPreparedStmt::*)()>(&Perl_MySQLPreparedStmt::Execute));
package.add("execute", static_cast<void(Perl_MySQLPreparedStmt::*)(perl::array)>(&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
+50
View File
@@ -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<mysql::PreparedStmt>(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<mysql::PreparedStmt> m_stmt;
mysql::StmtResult m_res = {};
perl::array m_row_array; // perf: cache for fetches
perl::hash m_row_hash;
};
+57
View File
@@ -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<Database, Deleter>(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>(&database, Deleter(false));
}
else if (type == Connection::Content)
{
m_db = std::unique_ptr<Database, Deleter>(&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<int>(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;
}
};
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <memory>
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<Database, Deleter> m_db;
};
+22 -7
View File
@@ -777,6 +777,8 @@ void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, st
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*inst, true);
}
items.clear();
}
// Only enforce trade rules if the NPC doesn't have an EVENT_TRADE
// subroutine. That overrides all.
@@ -2606,6 +2608,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));
@@ -2912,10 +2915,11 @@ void Client::SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions a
auto outapp = new ServerPacket(ServerOP_TraderMessaging, sizeof(TraderMessaging_Struct));
auto data = (TraderMessaging_Struct *) outapp->pBuffer;
data->action = action;
data->entity_id = trader->GetID();
data->trader_id = trader->CharacterID();
data->zone_id = trader->GetZoneID();
data->action = action;
data->entity_id = trader->GetID();
data->trader_id = trader->CharacterID();
data->zone_id = trader->GetZoneID();
data->instance_id = trader->GetInstanceID();
strn0cpy(data->trader_name, trader->GetName(), sizeof(data->trader_name));
worldserver.SendPacket(outapp);
@@ -3234,7 +3238,10 @@ void Client::SendBulkBazaarTraders()
void Client::DoBazaarInspect(const BazaarInspect_Struct &in)
{
auto items = TraderRepository::GetWhere(database, fmt::format("item_sn = {}", in.serial_number));
auto items = TraderRepository::GetWhere(
database, fmt::format("`char_id` = '{}' AND `item_sn` = '{}'", in.trader_id, in.serial_number)
);
if (items.empty()) {
LogInfo("Failed to find item with serial number [{}]", in.serial_number);
return;
@@ -3303,7 +3310,7 @@ std::string Client::DetermineMoneyString(uint64 cp)
void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicationPacket *app)
{
auto in = (TraderBuy_Struct *) app->pBuffer;
auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number);
auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number, tbs->trader_id);
if (!trader_item.id) {
LogTrading("Attempt to purchase an item outside of the Bazaar trader_id <red>[{}] item serial_number "
"<red>[{}] The Traders data was outdated.",
@@ -3497,7 +3504,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<int32>(tbs->quantity)) {
if (trader_item.item_charges <= static_cast<int32>(tbs->quantity) || !buy_item->IsStackable()) {
TraderRepository::DeleteOne(database, trader_item.id);
} else {
TraderRepository::UpdateQuantity(
@@ -4252,6 +4259,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 <red>[{}] 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 <red>[{}] Barter Sell/Buy Transaction Failed.", seller_error);
SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure);
+2 -1
View File
@@ -3942,7 +3942,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
c.second->QueuePacket(outapp);
safe_delete(outapp);
}
if (zone && zone->GetZoneID() == Zones::BAZAAR) {
if (zone && zone->GetZoneID() == Zones::BAZAAR && in->instance_id == zone->GetInstanceID()) {
if (in->action == TraderOn) {
c.second->SendBecomeTrader(TraderOn, in->entity_id);
}
@@ -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;