Compare commits

...

36 Commits

Author SHA1 Message Date
Chris Miles 8203c034bf [Database] Add indexes for data_buckets and zone_state_spawns (#4771)
* [Database] Add indexes for data_buckets and zone_state_spawns

* Update database_update_manifest.cpp

* Update database_update_manifest.cpp

* Update package.json
2025-03-11 03:29:26 -05:00
Akkadius 33ae51f56f [Release] 23.3.1 2025-03-11 01:38:59 -05:00
Chris Miles 30c39194a3 [Zone] Zone State Improvements (Continued) (#4768)
* [Zone] Zone State Improvements (Continued)

* Ignore a few events when we resume from suspend

* Add is_zone field

* Update database_update_manifest.cpp

* Update database_update_manifest.cpp

* Update database_update_manifest.cpp

* Update zone_save_state.cpp

* Update zone_save_state.cpp

* Add Zone Variables

* Update methods

* Update zone_save_state.cpp

* Update zone_save_state.cpp

---------

Co-authored-by: Kinglykrab <kinglykrab@gmail.com>
2025-03-11 01:14:09 -05:00
hg 051ce3736f [DynamicZones] Bulk request dz member statuses on zone boot (#4769)
When dynamic zones are cached on zone boot each dz requests member
statuses from world separately. This causes a lot of network traffic
between world and booted zones when there are a lot of active dzs.

This changes it to make a single request to world on zone boot and a
single bulk reply back.
2025-03-11 01:13:29 -05:00
Mitch Freeman 84708edccf Update client_evolving_items.cpp (#4767) 2025-03-09 12:20:37 -04:00
Chris Miles da4e9ab95b [Release] 23.3.0 (#4766) 2025-03-08 03:18:40 -06:00
Chris Miles a8fea95eab [Zone] State Save Improvements (#4765)
* [Zone] State saving improvements

* Make sure we load spawn enabled off of the state row

* Update npc.h

* Update spawn2.cpp

* Update database_instances.cpp

* Update database_instances.cpp
2025-03-08 03:15:42 -06:00
Mitch Freeman 53610c2f0f [Feature] Add Rule for dealing with augments when an item evolves (#4758) 2025-03-07 23:00:11 -06:00
catapultam-habeo 9f10c12874 [Bug Fix] Correct incorrectly calculated stat caps with Heroic Stats (#4760)
* fix incorrectly caclulcated stat caps

* fix typos and formatting
2025-03-07 22:59:47 -06:00
catapultam-habeo a0634adb3c [Feature] Allow assigning Helm Texture independently of Body Texture for Horses (#4759) 2025-03-07 22:59:12 -06:00
zimp-wow a2ed6be1f5 [Bug Fix] Zero out currentnpcid whenever spawn is reset. (#4763) 2025-03-07 22:38:23 -06:00
nytmyr c33ac40567 [Cleanup] Fix typo in GM tradeskill combine message (#4762) 2025-03-07 22:33:43 -06:00
Mitch Freeman 9ee095b354 [Fix] Add crash checks for certain PlayerEventLogs (#4761) 2025-03-07 16:17:36 -06:00
Alex King 7ab32af4dc [Rules] Fix EvolvingItems:PercentOfRaidExperience Description (#4757)
- Fixes an issue where the description was inaccurate, described [here](https://discord.com/channels/212663220849213441/557677602706423982/1347293634453835899).
2025-03-06 23:49:36 -05:00
Chris Miles 0c301419c2 [Zone] Make zone controller less likely to be visible, immune to all forms of combat (#4750)
* [Zone] Make zone controller less likely to be visible, immune to all forms of combat

* Exclude zone controller from scanning
2025-03-06 17:08:08 -05:00
nytmyr d6a21be25e [Bots] Fix taunting bots positioning (#4754)
* [Bots] Fix taunting bots positioning

- Fixes taunting bots liking to hug their target on certain models or chosen positions.
- Makes bots have a more realistic combat range in comparison to players.
- Removed unnecessary rules and checks for melee distance.

* Update ruletypes.h
2025-03-06 17:07:38 -05:00
nytmyr 1d4ba082ad [Bots] Move commanded spell map to zone (#4755)
- Moves the mapping of commanded spell min levels to zone rather than to each individual bot.
- Adds support for sub spell types.
2025-03-06 17:05:36 -05:00
nytmyr 94553501ba [Bots] Fix buffs not overwriting lesser buffs (#4756) 2025-03-06 17:03:50 -05:00
nytmyr da824d5178 [Crash] Bot aura crash fix (#4752)
- Something between the latest release caused this crash to appear, unsure of this exact cause.
- Prevents bots from being sent a spawn packet for Auras.
- Removes the bot's auras on Depop
2025-03-06 01:12:24 -05:00
Mitch Freeman 5a1df38900 [Fix] Parcel Delivery Updates for two edge cases (#4753)
- Properly send an item via parcel that has 0 charges
2025-03-06 01:12:02 -05:00
nytmyr 8cd7148b29 [Pets] Fix renamed pets loading as blank names (#4751) 2025-03-05 14:31:16 -05:00
Akkadius 09e079a45e [Hotfix] Forgot to push up some changes for test output 2025-03-04 17:18:02 -06:00
Chris Miles 4bc881da4b [Tests] Cleanup Hand-in Tests (#4749) 2025-03-04 17:15:27 -06:00
Chris Miles 0615864d51 [Databuckets] Nested Databuckets Protections and Improvements (#4748)
* Check for valid JSON before using it

* Do not allow nested keys to set be set an expiration

* Prevent overwriting of existing object or array

* Nested deletion support

* Update data_bucket.cpp

* Test cases

* More test cases, fix

* Update databuckets.cpp

* Update databuckets.cpp

* Basic databucket tests

* Update databuckets.cpp

* Update databuckets.cpp
2025-03-04 13:16:21 -06:00
Alex King 3638d157b2 [Logging] Convert JSON Error to Data Buckets Logging Category (#4747) 2025-03-03 23:16:47 -06:00
Akkadius d41725e325 [Hotfix] Fix sigabort crash from invalid JSON 2025-03-03 01:47:34 -06:00
Akkadius 88580b69b6 [Hotfix] Remove one port check in world 2025-03-03 00:49:02 -06:00
Akkadius f7a6fe595a [Release] 23.2.0 2025-03-03 00:17:54 -06:00
Chris Miles 07d14c2681 [Crash] Fix crash in add loot code path (#4745) 2025-03-03 00:16:51 -06:00
Chris Miles eac7a73fb6 [Crash] Potential crash fix in scan close mobs (#4744) 2025-03-03 00:16:42 -06:00
Chris Miles c5715f1f14 [Crash] Fix Aura process crash with bots (#4743) 2025-03-03 00:13:00 -06:00
Chris Miles 3902230fa1 [Crash] Fix world repop crash (#4742) 2025-03-02 22:04:39 -08:00
Chris Miles 212969f5cd [Crash] Database SetMutex crash fix (#4741) 2025-03-02 22:04:24 -08:00
Chris Miles de4226fdc9 [World] Check if port in use to avoid double booting mistakes (#4740)
* Stuff

* Potentially fix aura crash

* Reload crash fix

* Revert "Reload crash fix"

This reverts commit 96e1e76306.

* Fix

* Update entity.cpp

* Update dbcore.cpp

* [World] Check if port in use to avoid double booting mistakes

* Revert "Stuff"

This reverts commit 2162c00edd.

* Revert "Potentially fix aura crash"

This reverts commit 7c242723f4.

* Revert "Fix"

This reverts commit 8419e284d4.

* Revert "Update entity.cpp"

This reverts commit 8a1f4545a4.

* Revert "Update dbcore.cpp"

This reverts commit f0278d9591.
2025-03-02 22:04:03 -08:00
zimp-wow 8b13434197 [Bug Fix] Cleanup zone buckets on instance purge. (#4739) 2025-03-02 17:01:25 -06:00
catapultam-habeo 27274397ec [Bug Fix] Fix an error causing Endurance Regen to not be applied by items. (#4738)
* fix typo causing endurance regen to not be applied by items

* further correction
2025-03-02 03:39:22 -05:00
67 changed files with 1749 additions and 631 deletions
+85
View File
@@ -1,3 +1,88 @@
## [23.3.2] 3/11/2025
### DynamicZones
* Bulk request dz member statuses on zone boot ([#4769](https://github.com/EQEmu/Server/pull/4769)) @hgtw 2025-03-11
### Zone
* Zone State Improvements (Continued) ([#4768](https://github.com/EQEmu/Server/pull/4768)) @Akkadius 2025-03-11
## [23.3.0] 3/8/2025
### Bots
* Fix buffs not overwriting lesser buffs ([#4756](https://github.com/EQEmu/Server/pull/4756)) @nytmyr 2025-03-06
* Fix taunting bots positioning ([#4754](https://github.com/EQEmu/Server/pull/4754)) @nytmyr 2025-03-06
* Move commanded spell map to zone ([#4755](https://github.com/EQEmu/Server/pull/4755)) @nytmyr 2025-03-06
### Code
* Fix typo in GM tradeskill combine message ([#4762](https://github.com/EQEmu/Server/pull/4762)) @nytmyr 2025-03-08
### Crash
* Bot aura crash fix ([#4752](https://github.com/EQEmu/Server/pull/4752)) @nytmyr 2025-03-06
### Databuckets
* Nested Databuckets Protections and Improvements ([#4748](https://github.com/EQEmu/Server/pull/4748)) @Akkadius 2025-03-04
### Feature
* Add Rule for dealing with augments when an item evolves ([#4758](https://github.com/EQEmu/Server/pull/4758)) @neckkola 2025-03-08
* Allow assigning Helm Texture independently of Body Texture for Horses ([#4759](https://github.com/EQEmu/Server/pull/4759)) @catapultam-habeo 2025-03-08
### Fixes
* Add crash checks for certain PlayerEventLogs ([#4761](https://github.com/EQEmu/Server/pull/4761)) @neckkola 2025-03-07
* Correct incorrectly calculated stat caps with Heroic Stats ([#4760](https://github.com/EQEmu/Server/pull/4760)) @catapultam-habeo 2025-03-08
* Fix sigabort crash from invalid JSON @Akkadius 2025-03-03
* Forgot to push up some changes for test output @Akkadius 2025-03-04
* Parcel Delivery Updates for two edge cases ([#4753](https://github.com/EQEmu/Server/pull/4753)) @neckkola 2025-03-06
* Remove one port check in world @Akkadius 2025-03-03
* Zero out currentnpcid whenever spawn is reset. ([#4763](https://github.com/EQEmu/Server/pull/4763)) @zimp-wow 2025-03-08
### Logging
* Convert JSON Error to Data Buckets Logging Category ([#4747](https://github.com/EQEmu/Server/pull/4747)) @Kinglykrab 2025-03-04
### Pets
* Fix renamed pets loading as blank names ([#4751](https://github.com/EQEmu/Server/pull/4751)) @nytmyr 2025-03-05
### Rules
* Fix EvolvingItems:PercentOfRaidExperience Description ([#4757](https://github.com/EQEmu/Server/pull/4757)) @Kinglykrab 2025-03-07
### Tests
* Cleanup Hand-in Tests ([#4749](https://github.com/EQEmu/Server/pull/4749)) @Akkadius 2025-03-04
### Zone
* Make zone controller less likely to be visible, immune to all forms of combat ([#4750](https://github.com/EQEmu/Server/pull/4750)) @Akkadius 2025-03-06
* State Save Improvements ([#4765](https://github.com/EQEmu/Server/pull/4765)) @Akkadius 2025-03-08
## [23.2.0] 3/3/2025
### Crash
* Database SetMutex crash fix ([#4741](https://github.com/EQEmu/Server/pull/4741)) @Akkadius 2025-03-03
* Fix Aura process crash with bots ([#4743](https://github.com/EQEmu/Server/pull/4743)) @Akkadius 2025-03-03
* Fix crash in add loot code path ([#4745](https://github.com/EQEmu/Server/pull/4745)) @Akkadius 2025-03-03
* Fix world repop crash ([#4742](https://github.com/EQEmu/Server/pull/4742)) @Akkadius 2025-03-03
* Potential crash fix in scan close mobs ([#4744](https://github.com/EQEmu/Server/pull/4744)) @Akkadius 2025-03-03
### Fixes
* Cleanup zone buckets on instance purge. ([#4739](https://github.com/EQEmu/Server/pull/4739)) @zimp-wow 2025-03-02
* Fix an error causing Endurance Regen to not be applied by items. ([#4738](https://github.com/EQEmu/Server/pull/4738)) @catapultam-habeo 2025-03-02
### World
* Check if port in use to avoid double booting mistakes ([#4740](https://github.com/EQEmu/Server/pull/4740)) @Akkadius 2025-03-03
## [23.1.0] 3/1/2025
### Bots
+55 -3
View File
@@ -6845,7 +6845,7 @@ RENAME TABLE `expedition_lockouts` TO `dynamic_zone_lockouts`;
.condition = "empty",
.match = "",
.sql = R"(
-- Drop old indexes
-- Drop old indexes if exists
DROP INDEX IF EXISTS `keys` ON `data_buckets`;
DROP INDEX IF EXISTS `idx_npc_expires` ON `data_buckets`;
DROP INDEX IF EXISTS `idx_bot_expires` ON `data_buckets`;
@@ -6863,10 +6863,10 @@ ALTER TABLE `data_buckets`
MODIFY COLUMN `npc_id` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `character_id`,
MODIFY COLUMN `bot_id` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `npc_id`;
-- Create optimized unique index with `key` first
-- Create optimized unique index with `key` first
CREATE UNIQUE INDEX `keys` ON data_buckets (`key`, character_id, npc_id, bot_id, account_id, zone_id, instance_id);
-- Create indexes for just instance_id (instance deletion)
-- Create indexes for just instance_id (instance deletion)
CREATE INDEX idx_instance_id ON data_buckets (instance_id);
)",
.content_schema_update = false
@@ -6938,6 +6938,58 @@ CREATE TABLE `character_pet_name` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
)",
},
ManifestEntry{
.version = 9310,
.description = "2025_03_7_expand_horse_def.sql",
.check = "SHOW COLUMNS FROM `horses` LIKE 'helmtexture'",
.condition = "missing",
.match = "TINYINT(2)",
.sql = R"(
ALTER TABLE `horses`
ADD COLUMN `helmtexture` TINYINT(2) NOT NULL DEFAULT -1 AFTER `texture`;
)",
.content_schema_update = true
},
ManifestEntry{
.version = 9311,
.description = "2025_03_09_add_zone_state_is_zone_field.sql",
.check = "SHOW COLUMNS FROM `zone_state_spawns` LIKE 'is_zone'",
.condition = "empty",
.match = "",
.sql = R"(
ALTER TABLE `zone_state_spawns`
ADD COLUMN `is_zone` tinyint(11) NULL DEFAULT 0 AFTER `is_corpse`;
)",
.content_schema_update = false
},
ManifestEntry{
.version = 9312,
.description = "2025_03_11_data_bucket_indexes.sql",
.check = "SHOW INDEX FROM data_buckets",
.condition = "missing",
.match = "idx_zone_instance_expires",
.sql = R"(
DROP INDEX IF EXISTS `idx_zone_instance_expires` ON `data_buckets`;
DROP INDEX IF EXISTS `idx_character_expires` ON `data_buckets`;
DROP INDEX IF EXISTS `idx_bot_expires` ON `data_buckets`;
ALTER TABLE data_buckets ADD INDEX idx_zone_instance_expires (zone_id, instance_id, expires);
ALTER TABLE data_buckets ADD INDEX idx_character_expires (character_id, expires);
ALTER TABLE data_buckets ADD INDEX idx_bot_expires (bot_id, expires);
)",
.content_schema_update = false
},
ManifestEntry{
.version = 9313,
.description = "2025_03_11_data_bucket_indexes.sql",
.check = "SHOW INDEX FROM zone_state_spawns",
.condition = "missing",
.match = "idx_zone_instance",
.sql = R"(
ALTER TABLE zone_state_spawns ADD INDEX idx_zone_instance (zone_id, instance_id);
ALTER TABLE zone_state_spawns ADD INDEX idx_instance_id (instance_id);
)",
.content_schema_update = false
},
// -- template; copy/paste this when you need to create a new entry
// ManifestEntry{
// .version = 9228,
+8 -1
View File
@@ -31,7 +31,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include "../common/repositories/spawn_condition_values_repository.h"
#include "repositories/spawn2_disabled_repository.h"
#include "repositories/data_buckets_repository.h"
#include "repositories/zone_state_spawns_repository.h"
#include "database.h"
#include <iomanip>
@@ -480,6 +480,9 @@ void Database::DeleteInstance(uint16 instance_id)
DynamicZonesRepository::DeleteWhere(*this, fmt::format("instance_id = {}", instance_id));
CharacterCorpsesRepository::BuryInstance(*this, instance_id);
DataBucketsRepository::DeleteWhere(*this, fmt::format("instance_id = {}", instance_id));
if (RuleB(Zone, StateSavingOnShutdown)) {
ZoneStateSpawnsRepository::DeleteWhere(*this, fmt::format("`instance_id` = {}", instance_id));
}
}
void Database::FlagInstanceByGroupLeader(uint32 zone_id, int16 version, uint32 character_id, uint32 group_id)
@@ -562,6 +565,10 @@ void Database::PurgeExpiredInstances()
DynamicZoneMembersRepository::DeleteByManyInstances(*this, imploded_instance_ids);
DynamicZonesRepository::DeleteWhere(*this, fmt::format("instance_id IN ({})", imploded_instance_ids));
Spawn2DisabledRepository::DeleteWhere(*this, fmt::format("instance_id IN ({})", imploded_instance_ids));
DataBucketsRepository::DeleteWhere(*this, fmt::format("instance_id != 0 and instance_id IN ({})", imploded_instance_ids));
if (RuleB(Zone, StateSavingOnShutdown)) {
ZoneStateSpawnsRepository::DeleteWhere(*this, fmt::format("`instance_id` IN ({})", imploded_instance_ids));
}
}
void Database::SetInstanceDuration(uint16 instance_id, uint32 new_duration)
+3 -1
View File
@@ -302,7 +302,9 @@ std::string DBcore::Escape(const std::string& s)
void DBcore::SetMutex(Mutex *mutex)
{
safe_delete(m_mutex);
if (m_mutex && m_mutex != mutex) {
safe_delete(m_mutex);
}
DBcore::m_mutex = mutex;
}
+9 -1
View File
@@ -614,7 +614,7 @@ void EQEmuLogSys::EnableConsoleLogging()
std::copy(std::begin(pre_silence_settings), std::end(pre_silence_settings), std::begin(log_settings));
}
EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings()
EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings(bool silent_load)
{
InjectTablesIfNotExist();
@@ -699,6 +699,10 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings()
return this;
}
if (silent_load) {
SilenceConsoleLogging();
}
LogInfo("Loaded [{}] log categories", categories.size());
auto webhooks = DiscordWebhooksRepository::GetWhere(*m_database, fmt::format("id < {}", MAX_DISCORD_WEBHOOK_ID));
@@ -716,6 +720,10 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings()
log_settings[Logs::Info].log_to_file = static_cast<uint8>(Logs::General);
log_settings[Logs::Info].log_to_console = static_cast<uint8>(Logs::General);
if (silent_load) {
SilenceConsoleLogging();
}
return this;
}
+1 -1
View File
@@ -279,7 +279,7 @@ public:
*/
void CloseFileLogs();
EQEmuLogSys *LoadLogSettingsDefaults();
EQEmuLogSys *LoadLogDatabaseSettings();
EQEmuLogSys *LoadLogDatabaseSettings(bool silent_load = false);
/**
* @param directory_name
+73
View File
@@ -259,3 +259,76 @@ bool IpUtil::IsIPAddress(const std::string &ip_address)
}
#include <iostream>
#ifdef _WIN32
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // Link against Winsock library
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
#include <iostream>
#include <string>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h> // For inet_pton
#pragma comment(lib, "ws2_32.lib") // Link against Winsock library
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // For inet_pton
#include <unistd.h>
#endif
bool IpUtil::IsPortInUse(const std::string& ip, int port) {
bool in_use = false;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed\n";
return true; // Assume in use on failure
}
#endif
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
#ifdef _WIN32
WSACleanup();
#endif
return true; // Assume in use on failure
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
// Convert IP address from string to binary format
if (inet_pton(AF_INET, ip.c_str(), &addr.sin_addr) <= 0) {
std::cerr << "Invalid IP address format: " << ip << std::endl;
#ifdef _WIN32
closesocket(sock);
WSACleanup();
#else
close(sock);
#endif
return true; // Assume in use on failure
}
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
in_use = true; // Bind failed, port is in use
}
#ifdef _WIN32
closesocket(sock);
WSACleanup();
#else
close(sock);
#endif
return in_use;
}
+1
View File
@@ -37,6 +37,7 @@ public:
int port
);
static bool IsIPAddress(const std::string &ip_address);
static bool IsPortInUse(const std::string& ip, int port);
};
@@ -24,6 +24,7 @@ public:
int16_t race;
int8_t gender;
int8_t texture;
int8_t helmtexture;
float mountspeed;
std::string notes;
};
@@ -41,6 +42,7 @@ public:
"race",
"gender",
"texture",
"helmtexture",
"mountspeed",
"notes",
};
@@ -54,6 +56,7 @@ public:
"race",
"gender",
"texture",
"helmtexture",
"mountspeed",
"notes",
};
@@ -96,13 +99,14 @@ public:
{
Horses e{};
e.id = 0;
e.filename = "";
e.race = 216;
e.gender = 0;
e.texture = 0;
e.mountspeed = 0.75;
e.notes = "Notes";
e.id = 0;
e.filename = "";
e.race = 216;
e.gender = 0;
e.texture = 0;
e.helmtexture = -1;
e.mountspeed = 0.75;
e.notes = "Notes";
return e;
}
@@ -139,13 +143,14 @@ public:
if (results.RowCount() == 1) {
Horses e{};
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.mountspeed = row[5] ? strtof(row[5], nullptr) : 0.75;
e.notes = row[6] ? row[6] : "Notes";
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.helmtexture = row[5] ? static_cast<int8_t>(atoi(row[5])) : -1;
e.mountspeed = row[6] ? strtof(row[6], nullptr) : 0.75;
e.notes = row[7] ? row[7] : "Notes";
return e;
}
@@ -183,8 +188,9 @@ public:
v.push_back(columns[2] + " = " + std::to_string(e.race));
v.push_back(columns[3] + " = " + std::to_string(e.gender));
v.push_back(columns[4] + " = " + std::to_string(e.texture));
v.push_back(columns[5] + " = " + std::to_string(e.mountspeed));
v.push_back(columns[6] + " = '" + Strings::Escape(e.notes) + "'");
v.push_back(columns[5] + " = " + std::to_string(e.helmtexture));
v.push_back(columns[6] + " = " + std::to_string(e.mountspeed));
v.push_back(columns[7] + " = '" + Strings::Escape(e.notes) + "'");
auto results = db.QueryDatabase(
fmt::format(
@@ -211,6 +217,7 @@ public:
v.push_back(std::to_string(e.race));
v.push_back(std::to_string(e.gender));
v.push_back(std::to_string(e.texture));
v.push_back(std::to_string(e.helmtexture));
v.push_back(std::to_string(e.mountspeed));
v.push_back("'" + Strings::Escape(e.notes) + "'");
@@ -247,6 +254,7 @@ public:
v.push_back(std::to_string(e.race));
v.push_back(std::to_string(e.gender));
v.push_back(std::to_string(e.texture));
v.push_back(std::to_string(e.helmtexture));
v.push_back(std::to_string(e.mountspeed));
v.push_back("'" + Strings::Escape(e.notes) + "'");
@@ -282,13 +290,14 @@ public:
for (auto row = results.begin(); row != results.end(); ++row) {
Horses e{};
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.mountspeed = row[5] ? strtof(row[5], nullptr) : 0.75;
e.notes = row[6] ? row[6] : "Notes";
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.helmtexture = row[5] ? static_cast<int8_t>(atoi(row[5])) : -1;
e.mountspeed = row[6] ? strtof(row[6], nullptr) : 0.75;
e.notes = row[7] ? row[7] : "Notes";
all_entries.push_back(e);
}
@@ -313,13 +322,14 @@ public:
for (auto row = results.begin(); row != results.end(); ++row) {
Horses e{};
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.mountspeed = row[5] ? strtof(row[5], nullptr) : 0.75;
e.notes = row[6] ? row[6] : "Notes";
e.id = row[0] ? static_cast<int32_t>(atoi(row[0])) : 0;
e.filename = row[1] ? row[1] : "";
e.race = row[2] ? static_cast<int16_t>(atoi(row[2])) : 216;
e.gender = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.texture = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.helmtexture = row[5] ? static_cast<int8_t>(atoi(row[5])) : -1;
e.mountspeed = row[6] ? strtof(row[6], nullptr) : 0.75;
e.notes = row[7] ? row[7] : "Notes";
all_entries.push_back(e);
}
@@ -399,6 +409,7 @@ public:
v.push_back(std::to_string(e.race));
v.push_back(std::to_string(e.gender));
v.push_back(std::to_string(e.texture));
v.push_back(std::to_string(e.helmtexture));
v.push_back(std::to_string(e.mountspeed));
v.push_back("'" + Strings::Escape(e.notes) + "'");
@@ -428,6 +439,7 @@ public:
v.push_back(std::to_string(e.race));
v.push_back(std::to_string(e.gender));
v.push_back(std::to_string(e.texture));
v.push_back(std::to_string(e.helmtexture));
v.push_back(std::to_string(e.mountspeed));
v.push_back("'" + Strings::Escape(e.notes) + "'");
@@ -23,6 +23,7 @@ public:
uint32_t zone_id;
uint32_t instance_id;
int8_t is_corpse;
int8_t is_zone;
int32_t decay_in_seconds;
uint32_t npc_id;
uint32_t spawn2_id;
@@ -61,6 +62,7 @@ public:
"zone_id",
"instance_id",
"is_corpse",
"is_zone",
"decay_in_seconds",
"npc_id",
"spawn2_id",
@@ -95,6 +97,7 @@ public:
"zone_id",
"instance_id",
"is_corpse",
"is_zone",
"decay_in_seconds",
"npc_id",
"spawn2_id",
@@ -163,6 +166,7 @@ public:
e.zone_id = 0;
e.instance_id = 0;
e.is_corpse = 0;
e.is_zone = 0;
e.decay_in_seconds = 0;
e.npc_id = 0;
e.spawn2_id = 0;
@@ -227,30 +231,31 @@ public:
e.zone_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.instance_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.is_corpse = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.decay_in_seconds = row[4] ? static_cast<int32_t>(atoi(row[4])) : 0;
e.npc_id = row[5] ? static_cast<uint32_t>(strtoul(row[5], nullptr, 10)) : 0;
e.spawn2_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawngroup_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.x = row[8] ? strtof(row[8], nullptr) : 0;
e.y = row[9] ? strtof(row[9], nullptr) : 0;
e.z = row[10] ? strtof(row[10], nullptr) : 0;
e.heading = row[11] ? strtof(row[11], nullptr) : 0;
e.respawn_time = row[12] ? static_cast<uint32_t>(strtoul(row[12], nullptr, 10)) : 0;
e.variance = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.grid = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.current_waypoint = row[15] ? static_cast<int32_t>(atoi(row[15])) : 0;
e.path_when_zone_idle = row[16] ? static_cast<int16_t>(atoi(row[16])) : 0;
e.condition_id = row[17] ? static_cast<uint16_t>(strtoul(row[17], nullptr, 10)) : 0;
e.condition_min_value = row[18] ? static_cast<int16_t>(atoi(row[18])) : 0;
e.enabled = row[19] ? static_cast<int16_t>(atoi(row[19])) : 1;
e.anim = row[20] ? static_cast<uint16_t>(strtoul(row[20], nullptr, 10)) : 0;
e.loot_data = row[21] ? row[21] : "";
e.entity_variables = row[22] ? row[22] : "";
e.buffs = row[23] ? row[23] : "";
e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0;
e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10);
e.is_zone = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.decay_in_seconds = row[5] ? static_cast<int32_t>(atoi(row[5])) : 0;
e.npc_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawn2_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.spawngroup_id = row[8] ? static_cast<uint32_t>(strtoul(row[8], nullptr, 10)) : 0;
e.x = row[9] ? strtof(row[9], nullptr) : 0;
e.y = row[10] ? strtof(row[10], nullptr) : 0;
e.z = row[11] ? strtof(row[11], nullptr) : 0;
e.heading = row[12] ? strtof(row[12], nullptr) : 0;
e.respawn_time = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.variance = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.grid = row[15] ? static_cast<uint32_t>(strtoul(row[15], nullptr, 10)) : 0;
e.current_waypoint = row[16] ? static_cast<int32_t>(atoi(row[16])) : 0;
e.path_when_zone_idle = row[17] ? static_cast<int16_t>(atoi(row[17])) : 0;
e.condition_id = row[18] ? static_cast<uint16_t>(strtoul(row[18], nullptr, 10)) : 0;
e.condition_min_value = row[19] ? static_cast<int16_t>(atoi(row[19])) : 0;
e.enabled = row[20] ? static_cast<int16_t>(atoi(row[20])) : 1;
e.anim = row[21] ? static_cast<uint16_t>(strtoul(row[21], nullptr, 10)) : 0;
e.loot_data = row[22] ? row[22] : "";
e.entity_variables = row[23] ? row[23] : "";
e.buffs = row[24] ? row[24] : "";
e.hp = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.mana = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.endurance = row[27] ? strtoll(row[27], nullptr, 10) : 0;
e.created_at = strtoll(row[28] ? row[28] : "-1", nullptr, 10);
return e;
}
@@ -287,30 +292,31 @@ public:
v.push_back(columns[1] + " = " + std::to_string(e.zone_id));
v.push_back(columns[2] + " = " + std::to_string(e.instance_id));
v.push_back(columns[3] + " = " + std::to_string(e.is_corpse));
v.push_back(columns[4] + " = " + std::to_string(e.decay_in_seconds));
v.push_back(columns[5] + " = " + std::to_string(e.npc_id));
v.push_back(columns[6] + " = " + std::to_string(e.spawn2_id));
v.push_back(columns[7] + " = " + std::to_string(e.spawngroup_id));
v.push_back(columns[8] + " = " + std::to_string(e.x));
v.push_back(columns[9] + " = " + std::to_string(e.y));
v.push_back(columns[10] + " = " + std::to_string(e.z));
v.push_back(columns[11] + " = " + std::to_string(e.heading));
v.push_back(columns[12] + " = " + std::to_string(e.respawn_time));
v.push_back(columns[13] + " = " + std::to_string(e.variance));
v.push_back(columns[14] + " = " + std::to_string(e.grid));
v.push_back(columns[15] + " = " + std::to_string(e.current_waypoint));
v.push_back(columns[16] + " = " + std::to_string(e.path_when_zone_idle));
v.push_back(columns[17] + " = " + std::to_string(e.condition_id));
v.push_back(columns[18] + " = " + std::to_string(e.condition_min_value));
v.push_back(columns[19] + " = " + std::to_string(e.enabled));
v.push_back(columns[20] + " = " + std::to_string(e.anim));
v.push_back(columns[21] + " = '" + Strings::Escape(e.loot_data) + "'");
v.push_back(columns[22] + " = '" + Strings::Escape(e.entity_variables) + "'");
v.push_back(columns[23] + " = '" + Strings::Escape(e.buffs) + "'");
v.push_back(columns[24] + " = " + std::to_string(e.hp));
v.push_back(columns[25] + " = " + std::to_string(e.mana));
v.push_back(columns[26] + " = " + std::to_string(e.endurance));
v.push_back(columns[27] + " = FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")");
v.push_back(columns[4] + " = " + std::to_string(e.is_zone));
v.push_back(columns[5] + " = " + std::to_string(e.decay_in_seconds));
v.push_back(columns[6] + " = " + std::to_string(e.npc_id));
v.push_back(columns[7] + " = " + std::to_string(e.spawn2_id));
v.push_back(columns[8] + " = " + std::to_string(e.spawngroup_id));
v.push_back(columns[9] + " = " + std::to_string(e.x));
v.push_back(columns[10] + " = " + std::to_string(e.y));
v.push_back(columns[11] + " = " + std::to_string(e.z));
v.push_back(columns[12] + " = " + std::to_string(e.heading));
v.push_back(columns[13] + " = " + std::to_string(e.respawn_time));
v.push_back(columns[14] + " = " + std::to_string(e.variance));
v.push_back(columns[15] + " = " + std::to_string(e.grid));
v.push_back(columns[16] + " = " + std::to_string(e.current_waypoint));
v.push_back(columns[17] + " = " + std::to_string(e.path_when_zone_idle));
v.push_back(columns[18] + " = " + std::to_string(e.condition_id));
v.push_back(columns[19] + " = " + std::to_string(e.condition_min_value));
v.push_back(columns[20] + " = " + std::to_string(e.enabled));
v.push_back(columns[21] + " = " + std::to_string(e.anim));
v.push_back(columns[22] + " = '" + Strings::Escape(e.loot_data) + "'");
v.push_back(columns[23] + " = '" + Strings::Escape(e.entity_variables) + "'");
v.push_back(columns[24] + " = '" + Strings::Escape(e.buffs) + "'");
v.push_back(columns[25] + " = " + std::to_string(e.hp));
v.push_back(columns[26] + " = " + std::to_string(e.mana));
v.push_back(columns[27] + " = " + std::to_string(e.endurance));
v.push_back(columns[28] + " = FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")");
auto results = db.QueryDatabase(
fmt::format(
@@ -336,6 +342,7 @@ public:
v.push_back(std::to_string(e.zone_id));
v.push_back(std::to_string(e.instance_id));
v.push_back(std::to_string(e.is_corpse));
v.push_back(std::to_string(e.is_zone));
v.push_back(std::to_string(e.decay_in_seconds));
v.push_back(std::to_string(e.npc_id));
v.push_back(std::to_string(e.spawn2_id));
@@ -393,6 +400,7 @@ public:
v.push_back(std::to_string(e.zone_id));
v.push_back(std::to_string(e.instance_id));
v.push_back(std::to_string(e.is_corpse));
v.push_back(std::to_string(e.is_zone));
v.push_back(std::to_string(e.decay_in_seconds));
v.push_back(std::to_string(e.npc_id));
v.push_back(std::to_string(e.spawn2_id));
@@ -454,30 +462,31 @@ public:
e.zone_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.instance_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.is_corpse = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.decay_in_seconds = row[4] ? static_cast<int32_t>(atoi(row[4])) : 0;
e.npc_id = row[5] ? static_cast<uint32_t>(strtoul(row[5], nullptr, 10)) : 0;
e.spawn2_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawngroup_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.x = row[8] ? strtof(row[8], nullptr) : 0;
e.y = row[9] ? strtof(row[9], nullptr) : 0;
e.z = row[10] ? strtof(row[10], nullptr) : 0;
e.heading = row[11] ? strtof(row[11], nullptr) : 0;
e.respawn_time = row[12] ? static_cast<uint32_t>(strtoul(row[12], nullptr, 10)) : 0;
e.variance = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.grid = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.current_waypoint = row[15] ? static_cast<int32_t>(atoi(row[15])) : 0;
e.path_when_zone_idle = row[16] ? static_cast<int16_t>(atoi(row[16])) : 0;
e.condition_id = row[17] ? static_cast<uint16_t>(strtoul(row[17], nullptr, 10)) : 0;
e.condition_min_value = row[18] ? static_cast<int16_t>(atoi(row[18])) : 0;
e.enabled = row[19] ? static_cast<int16_t>(atoi(row[19])) : 1;
e.anim = row[20] ? static_cast<uint16_t>(strtoul(row[20], nullptr, 10)) : 0;
e.loot_data = row[21] ? row[21] : "";
e.entity_variables = row[22] ? row[22] : "";
e.buffs = row[23] ? row[23] : "";
e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0;
e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10);
e.is_zone = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.decay_in_seconds = row[5] ? static_cast<int32_t>(atoi(row[5])) : 0;
e.npc_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawn2_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.spawngroup_id = row[8] ? static_cast<uint32_t>(strtoul(row[8], nullptr, 10)) : 0;
e.x = row[9] ? strtof(row[9], nullptr) : 0;
e.y = row[10] ? strtof(row[10], nullptr) : 0;
e.z = row[11] ? strtof(row[11], nullptr) : 0;
e.heading = row[12] ? strtof(row[12], nullptr) : 0;
e.respawn_time = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.variance = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.grid = row[15] ? static_cast<uint32_t>(strtoul(row[15], nullptr, 10)) : 0;
e.current_waypoint = row[16] ? static_cast<int32_t>(atoi(row[16])) : 0;
e.path_when_zone_idle = row[17] ? static_cast<int16_t>(atoi(row[17])) : 0;
e.condition_id = row[18] ? static_cast<uint16_t>(strtoul(row[18], nullptr, 10)) : 0;
e.condition_min_value = row[19] ? static_cast<int16_t>(atoi(row[19])) : 0;
e.enabled = row[20] ? static_cast<int16_t>(atoi(row[20])) : 1;
e.anim = row[21] ? static_cast<uint16_t>(strtoul(row[21], nullptr, 10)) : 0;
e.loot_data = row[22] ? row[22] : "";
e.entity_variables = row[23] ? row[23] : "";
e.buffs = row[24] ? row[24] : "";
e.hp = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.mana = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.endurance = row[27] ? strtoll(row[27], nullptr, 10) : 0;
e.created_at = strtoll(row[28] ? row[28] : "-1", nullptr, 10);
all_entries.push_back(e);
}
@@ -506,30 +515,31 @@ public:
e.zone_id = row[1] ? static_cast<uint32_t>(strtoul(row[1], nullptr, 10)) : 0;
e.instance_id = row[2] ? static_cast<uint32_t>(strtoul(row[2], nullptr, 10)) : 0;
e.is_corpse = row[3] ? static_cast<int8_t>(atoi(row[3])) : 0;
e.decay_in_seconds = row[4] ? static_cast<int32_t>(atoi(row[4])) : 0;
e.npc_id = row[5] ? static_cast<uint32_t>(strtoul(row[5], nullptr, 10)) : 0;
e.spawn2_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawngroup_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.x = row[8] ? strtof(row[8], nullptr) : 0;
e.y = row[9] ? strtof(row[9], nullptr) : 0;
e.z = row[10] ? strtof(row[10], nullptr) : 0;
e.heading = row[11] ? strtof(row[11], nullptr) : 0;
e.respawn_time = row[12] ? static_cast<uint32_t>(strtoul(row[12], nullptr, 10)) : 0;
e.variance = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.grid = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.current_waypoint = row[15] ? static_cast<int32_t>(atoi(row[15])) : 0;
e.path_when_zone_idle = row[16] ? static_cast<int16_t>(atoi(row[16])) : 0;
e.condition_id = row[17] ? static_cast<uint16_t>(strtoul(row[17], nullptr, 10)) : 0;
e.condition_min_value = row[18] ? static_cast<int16_t>(atoi(row[18])) : 0;
e.enabled = row[19] ? static_cast<int16_t>(atoi(row[19])) : 1;
e.anim = row[20] ? static_cast<uint16_t>(strtoul(row[20], nullptr, 10)) : 0;
e.loot_data = row[21] ? row[21] : "";
e.entity_variables = row[22] ? row[22] : "";
e.buffs = row[23] ? row[23] : "";
e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0;
e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10);
e.is_zone = row[4] ? static_cast<int8_t>(atoi(row[4])) : 0;
e.decay_in_seconds = row[5] ? static_cast<int32_t>(atoi(row[5])) : 0;
e.npc_id = row[6] ? static_cast<uint32_t>(strtoul(row[6], nullptr, 10)) : 0;
e.spawn2_id = row[7] ? static_cast<uint32_t>(strtoul(row[7], nullptr, 10)) : 0;
e.spawngroup_id = row[8] ? static_cast<uint32_t>(strtoul(row[8], nullptr, 10)) : 0;
e.x = row[9] ? strtof(row[9], nullptr) : 0;
e.y = row[10] ? strtof(row[10], nullptr) : 0;
e.z = row[11] ? strtof(row[11], nullptr) : 0;
e.heading = row[12] ? strtof(row[12], nullptr) : 0;
e.respawn_time = row[13] ? static_cast<uint32_t>(strtoul(row[13], nullptr, 10)) : 0;
e.variance = row[14] ? static_cast<uint32_t>(strtoul(row[14], nullptr, 10)) : 0;
e.grid = row[15] ? static_cast<uint32_t>(strtoul(row[15], nullptr, 10)) : 0;
e.current_waypoint = row[16] ? static_cast<int32_t>(atoi(row[16])) : 0;
e.path_when_zone_idle = row[17] ? static_cast<int16_t>(atoi(row[17])) : 0;
e.condition_id = row[18] ? static_cast<uint16_t>(strtoul(row[18], nullptr, 10)) : 0;
e.condition_min_value = row[19] ? static_cast<int16_t>(atoi(row[19])) : 0;
e.enabled = row[20] ? static_cast<int16_t>(atoi(row[20])) : 1;
e.anim = row[21] ? static_cast<uint16_t>(strtoul(row[21], nullptr, 10)) : 0;
e.loot_data = row[22] ? row[22] : "";
e.entity_variables = row[23] ? row[23] : "";
e.buffs = row[24] ? row[24] : "";
e.hp = row[25] ? strtoll(row[25], nullptr, 10) : 0;
e.mana = row[26] ? strtoll(row[26], nullptr, 10) : 0;
e.endurance = row[27] ? strtoll(row[27], nullptr, 10) : 0;
e.created_at = strtoll(row[28] ? row[28] : "-1", nullptr, 10);
all_entries.push_back(e);
}
@@ -608,6 +618,7 @@ public:
v.push_back(std::to_string(e.zone_id));
v.push_back(std::to_string(e.instance_id));
v.push_back(std::to_string(e.is_corpse));
v.push_back(std::to_string(e.is_zone));
v.push_back(std::to_string(e.decay_in_seconds));
v.push_back(std::to_string(e.npc_id));
v.push_back(std::to_string(e.spawn2_id));
@@ -658,6 +669,7 @@ public:
v.push_back(std::to_string(e.zone_id));
v.push_back(std::to_string(e.instance_id));
v.push_back(std::to_string(e.is_corpse));
v.push_back(std::to_string(e.is_zone));
v.push_back(std::to_string(e.decay_in_seconds));
v.push_back(std::to_string(e.npc_id));
v.push_back(std::to_string(e.spawn2_id));
+11 -10
View File
@@ -374,6 +374,8 @@ RULE_BOOL(Zone, AllowCrossZoneSpellsOnBots, false, "Set to true to allow cross z
RULE_BOOL(Zone, AllowCrossZoneSpellsOnMercs, false, "Set to true to allow cross zone spells (cast/remove) to affect mercenaries")
RULE_BOOL(Zone, AllowCrossZoneSpellsOnPets, false, "Set to true to allow cross zone spells (cast/remove) to affect pets")
RULE_BOOL(Zone, ZoneShardQuestMenuOnly, false, "Set to true if you only want quests to show the zone shard menu")
RULE_BOOL(Zone, StateSaveEntityVariables, true, "Set to true if you want buffs to be saved on shutdown")
RULE_BOOL(Zone, StateSaveBuffs, true, "Set to true if you want buffs to be saved on shutdown")
RULE_BOOL(Zone, StateSavingOnShutdown, true, "Set to true if you want zones to save state on shutdown (npcs, corpses, loot, entity variables, buffs etc.)")
RULE_CATEGORY_END()
@@ -850,17 +852,15 @@ RULE_BOOL(Bots, BotArcheryConsumesAmmo, true, "Set to false to disable Archery A
RULE_BOOL(Bots, BotThrowingConsumesAmmo, true, "Set to false to disable Throwing Ammo Consumption")
RULE_INT(Bots, StackSizeMin, 20, "20 Default. -1 to disable and use default max stack size. Minimum stack size to give a bot (Arrows/Throwing).")
RULE_INT(Bots, HasOrMayGetAggroThreshold, 90, "90 Default. Percent threshold of total hate where bots will stop casting spells that generate hate if they are set to try to not pull aggro via spells.")
RULE_BOOL(Bots, UseFlatNormalMeleeRange, false, "False Default. If true, bots melee distance will be a flat distance set by Bots:NormalMeleeRangeDistance.")
RULE_REAL(Bots, NormalMeleeRangeDistance, 0.75, "If UseFlatNormalMeleeRange is enabled, multiplier of the max melee range at which a bot will stand in melee combat. 0.75 Recommended, max melee for all abilities to land.")
RULE_REAL(Bots, PercentMinMeleeDistance, 0.75, "Multiplier of the their melee range - Minimum distance from target a bot will stand while in melee combat before trying to adjust. 0.60 Recommended.")
RULE_REAL(Bots, MaxDistanceForMelee, 20, "Maximum distance bots will stand for melee. Default 20 to allow all special attacks to land.")
RULE_REAL(Bots, TauntNormalMeleeRangeDistance, 0.50, "Multiplier of the max melee range at which a taunting bot will stand in melee combat. 0.50 Recommended, closer than others .")
RULE_REAL(Bots, PercentTauntMinMeleeDistance, 0.40, "Multiplier of their melee range - Minimum distance from target a taunting bot will stand while in melee combat before trying to adjust. 0.25 Recommended.")
RULE_REAL(Bots, PercentMaxMeleeRangeDistance, 0.95, "Multiplier of the max melee range at which a bot will stand in melee combat. 0.95 Recommended, max melee while disabling special attacks/taunt.")
RULE_REAL(Bots, PercentMinMaxMeleeRangeDistance, 0.75, "Multiplier of the closest max melee range at which a bot will stand in melee combat before trying to adjust. 0.75 Recommended, max melee while disabling special attacks/taunt.")
RULE_REAL(Bots, LowerMeleeDistanceMultiplier, 0.35, "Closest % of the hit box a melee bot will get to the target. Default 0.35")
RULE_REAL(Bots, LowerTauntingMeleeDistanceMultiplier, 0.25, "Closest % of the hit box a taunting melee bot will get to the target. Default 0.25")
RULE_REAL(Bots, LowerMaxMeleeRangeDistanceMultiplier, 0.80, "Closest % of the hit box a max melee range melee bot will get to the target. Default 0.80")
RULE_REAL(Bots, UpperMeleeDistanceMultiplier, 0.55, "Furthest % of the hit box a melee bot will get from the target. Default 0.55")
RULE_REAL(Bots, UpperTauntingMeleeDistanceMultiplier, 0.45, "Furthest % of the hit box a taunting melee bot will get from the target. Default 0.45")
RULE_REAL(Bots, UpperMaxMeleeRangeDistanceMultiplier, 0.95, "Furthest % of the hit box a max melee range melee bot will get from the target. Default 0.95")
RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, true, "If true, when bots are at max melee distance, special abilities including taunt will be disabled. Default True.")
RULE_BOOL(Bots, TauntingBotsFollowTopHate, true, "True Default. If true, bots that are taunting will attempt to stick with whoever currently is top hate.")
RULE_INT(Bots, DistanceTauntingBotsStickMainHate, 10, "If TauntingBotsFollowTopHate is enabled, this is the distance bots will try to stick to whoever currently is Top Hate.")
RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, true, "True Default. If true, when bots are at max melee distance, special abilities including taunt will be disabled.")
RULE_INT(Bots, MinJitterTimer, 500, "Minimum ms between bot movement jitter checks.")
RULE_INT(Bots, MaxJitterTimer, 2500, "Maximum ms between bot movement jitter checks. Set to 0 to disable timer checks.")
RULE_BOOL(Bots, PreventBotCampOnFD, true, "True Default. If true, players will not be able to camp bots while feign death.")
@@ -1163,8 +1163,9 @@ RULE_CATEGORY_END()
RULE_CATEGORY(EvolvingItems)
RULE_REAL(EvolvingItems, PercentOfSoloExperience, 0.1, "Percentage of solo experience allocated to evolving items that require experience.")
RULE_REAL(EvolvingItems, PercentOfGroupExperience, 0.1, "Percentage of group experience allocated to evolving items that require experience.")
RULE_REAL(EvolvingItems, PercentOfRaidExperience, 0.1, "Percentage of solo experience allocated to evolving items that require experience.")
RULE_REAL(EvolvingItems, PercentOfRaidExperience, 0.1, "Percentage of raid experience allocated to evolving items that require experience.")
RULE_INT(EvolvingItems, DelayUponEquipping, 30000, "Delay in ms before an evolving item will earn rewards after equipping. Default is 30000ms or 30s.")
RULE_BOOL(EvolvingItems, DestroyAugmentsOnEvolve, false, "If this is enabled, any augments in an item will be destroyed when the item evolves. Otherwise, send augments to the player via the parcel system (requires that the Parcel System be enabled).")
RULE_CATEGORY_END()
#undef RULE_CATEGORY
+8
View File
@@ -196,6 +196,7 @@
#define ServerOP_DzSaveInvite 0x0466
#define ServerOP_DzRequestInvite 0x0467
#define ServerOP_DzMakeLeader 0x0468
#define ServerOP_DzGetBulkMemberStatuses 0x0469
#define ServerOP_LSInfo 0x1000
#define ServerOP_LSStatus 0x1001
@@ -1555,6 +1556,13 @@ struct ServerDzMemberStatuses_Struct {
ServerDzMemberStatusEntry_Struct entries[0];
};
struct ServerDzCerealData_Struct {
uint16_t zone_id;
uint16_t inst_id;
uint32_t cereal_size;
char cereal_data[1];
};
struct ServerDzMovePC_Struct {
uint32 dz_id;
uint16 sender_zone_id;
+6 -5
View File
@@ -736,11 +736,12 @@ namespace BotSpellTypes
constexpr uint16 DiscUtility = 203;
constexpr uint16 START = BotSpellTypes::Nuke; // Do not remove or change this
constexpr uint16 END = BotSpellTypes::PetResistBuffs; // Do not remove this, increment as needed
constexpr uint16 COMMANDED_START = BotSpellTypes::Lull; // Do not remove or change this
constexpr uint16 COMMANDED_END = BotSpellTypes::AELull; // Do not remove this, increment as needed
constexpr uint16 DISCIPLINE_START = BotSpellTypes::Discipline; // Do not remove or change this
constexpr uint16 DISCIPLINE_END = BotSpellTypes::DiscUtility; // Do not remove this, increment as needed
constexpr uint16 END = BotSpellTypes::PetResistBuffs; // Do not remove this, increment as needed
constexpr uint16 COMMANDED_START = BotSpellTypes::Lull; // Do not remove or change this
constexpr uint16 COMMANDED_END = BotSpellTypes::AELull; // Do not remove this, increment as needed
constexpr uint16 DISCIPLINE_START = BotSpellTypes::Discipline; // Do not remove or change this
constexpr uint16 DISCIPLINE_END = BotSpellTypes::DiscUtility; // Do not remove this, increment as needed
constexpr uint16 PARENT_TYPE_END = BotSpellTypes::PreCombatBuffSong; // This is the last ID of the original bot spell types, the rest are considered sub types.
}
static std::map<uint16, std::string> spell_type_names = {
+8
View File
@@ -936,3 +936,11 @@ std::string Strings::Slugify(const std::string& input, const std::string& separa
return slug;
}
bool Strings::IsValidJson(const std::string &json)
{
rapidjson::Document doc;
rapidjson::ParseResult result = doc.Parse(json.c_str());
return result;
}
+2
View File
@@ -45,6 +45,7 @@
#include <type_traits>
#include <fmt/format.h>
#include <cereal/external/rapidjson/document.h>
#ifndef _WIN32
// this doesn't appear to affect linux-based systems..need feedback for _WIN64
@@ -188,6 +189,7 @@ public:
}
static std::string Slugify(const std::string &input, const std::string &separator = "-");
static bool IsValidJson(const std::string& json);
};
const std::string StringFormat(const char *format, ...);
+2 -2
View File
@@ -25,7 +25,7 @@
// Build variables
// these get injected during the build pipeline
#define CURRENT_VERSION "23.1.0-dev" // always append -dev to the current version for custom-builds
#define CURRENT_VERSION "23.3.2-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 9309
#define CURRENT_BINARY_DATABASE_VERSION 9313
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054
#endif
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eqemu-server",
"version": "23.1.0",
"version": "23.3.2",
"repository": {
"type": "git",
"url": "https://github.com/EQEmu/Server.git"
+2 -1
View File
@@ -56,8 +56,9 @@ echo "# Running shared_memory"
echo "# Running NPC hand-in tests"
./bin/zone tests:npc-handins 2>&1 | tee test_output.log
./bin/zone tests:npc-handins-multiquest 2>&1 | tee -a test_output.log
./bin/zone tests:databuckets 2>&1 | tee -a test_output.log
if grep -E -q "QueryErr|Error" test_output.log; then
if grep -E -q "QueryErr|Error|FAILED" test_output.log; then
echo "Error found in test output! Failing build."
exit 1
fi
+37
View File
@@ -7,6 +7,7 @@
#include "zoneserver.h"
#include "../common/rulesys.h"
#include "../common/repositories/dynamic_zone_lockouts_repository.h"
#include <cereal/types/utility.hpp>
extern ClientList client_list;
extern ZSList zoneserver_list;
@@ -169,6 +170,33 @@ void DynamicZoneManager::LoadTemplates()
}
}
void DynamicZoneManager::SendBulkMemberStatuses(uint32_t zone_id, uint16_t inst_id)
{
std::vector<std::pair<uint32_t, std::vector<DynamicZoneMember>>> dzs;
dzs.reserve(dynamic_zone_cache.size());
for (const auto& [dz_id, dz] : dynamic_zone_cache)
{
dzs.emplace_back(dz_id, dz->GetMembers());
}
std::ostringstream ss;
{
cereal::BinaryOutputArchive archive(ss);
archive(dzs);
}
std::string_view sv = ss.view();
size_t size = sizeof(ServerDzCerealData_Struct) + sv.size();
ServerPacket pack(ServerOP_DzGetBulkMemberStatuses, static_cast<uint32_t>(size));
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack.pBuffer);
buf->cereal_size = static_cast<uint32_t>(sv.size());
memcpy(buf->cereal_data, sv.data(), sv.size());
zoneserver_list.SendPacket(zone_id, inst_id, &pack);
}
void DynamicZoneManager::HandleZoneMessage(ServerPacket* pack)
{
switch (pack->opcode)
@@ -338,6 +366,15 @@ void DynamicZoneManager::HandleZoneMessage(ServerPacket* pack)
}
break;
}
case ServerOP_DzGetBulkMemberStatuses:
{
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack->pBuffer);
if (buf->zone_id != 0 && !dynamic_zone_cache.empty())
{
SendBulkMemberStatuses(buf->zone_id, buf->inst_id);
}
break;
}
case ServerOP_DzUpdateMemberStatus:
{
auto buf = reinterpret_cast<ServerDzMemberStatus_Struct*>(pack->pBuffer);
+2
View File
@@ -30,6 +30,8 @@ public:
std::unordered_map<uint32_t, std::unique_ptr<DynamicZone>> dynamic_zone_cache;
private:
void SendBulkMemberStatuses(uint32_t zone_id, uint16_t inst_id);
Timer m_process_throttle_timer{};
std::unordered_map<uint32_t, DynamicZoneTemplatesRepository::DynamicZoneTemplates> m_dz_templates;
};
+6
View File
@@ -89,6 +89,7 @@
#include "../common/events/player_event_logs.h"
#include "../common/skill_caps.h"
#include "../common/repositories/character_parcels_repository.h"
#include "../common/ip_util.h"
SkillCaps skill_caps;
ZoneStore zone_store;
@@ -188,6 +189,11 @@ int main(int argc, char **argv)
launcher_list.LoadList();
zoneserver_list.Init();
if (IpUtil::IsPortInUse(Config->WorldIP, Config->WorldTCPPort)) {
LogError("World port [{}] already in use", Config->WorldTCPPort);
return 1;
}
std::unique_ptr<EQ::Net::ConsoleServer> console;
if (Config->TelnetEnabled) {
LogInfo("Console (TCP) listener started on [{}:{}]", Config->TelnetIP, Config->TelnetTCPPort);
+2 -1
View File
@@ -894,7 +894,8 @@ void ZSList::SendServerReload(ServerReload::Type type, uchar *packet)
ServerReload::Type::Commands,
ServerReload::Type::PerlExportSettings,
ServerReload::Type::DataBucketsCache,
ServerReload::Type::WorldRepop
ServerReload::Type::WorldRepop,
ServerReload::Type::WorldWithRespawn
};
// Set requires_zone_booted flag before executing reload logic
+1
View File
@@ -1480,6 +1480,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) {
case ServerOP_DzSwapMembers:
case ServerOP_DzRemoveAllMembers:
case ServerOP_DzGetMemberStatuses:
case ServerOP_DzGetBulkMemberStatuses:
case ServerOP_DzSetSecondsRemaining:
case ServerOP_DzSetCompass:
case ServerOP_DzSetSafeReturn:
+5 -1
View File
@@ -690,12 +690,16 @@ void Aura::ProcessSpawns()
continue;
}
if (!e.second->IsOfClientBot()) {
if (!e.second->IsClient()) {
continue;
}
auto c = e.second->CastToClient();
if (!c) {
continue;
}
bool spawned = spawned_for.find(c->GetID()) != spawned_for.end();
if (ShouldISpawnFor(c)) {
if (!spawned) {
+14 -14
View File
@@ -318,13 +318,13 @@ void Mob::AddItemBonuses(const EQ::ItemInstance* inst, StatBonuses* b, bool is_a
b->HeroicWIS += CalcItemBonus(item->HeroicWis);
b->HeroicCHA += CalcItemBonus(item->HeroicCha);
b->STRCapMod += b->HeroicSTR;
b->STACapMod += b->HeroicSTA;
b->DEXCapMod += b->HeroicDEX;
b->AGICapMod += b->HeroicAGI;
b->INTCapMod += b->HeroicINT;
b->WISCapMod += b->HeroicWIS;
b->CHACapMod += b->HeroicCHA;
b->STRCapMod += item->HeroicStr;
b->STACapMod += item->HeroicSta;
b->DEXCapMod += item->HeroicDex;
b->AGICapMod += item->HeroicAgi;
b->INTCapMod += item->HeroicInt;
b->WISCapMod += item->HeroicWis;
b->CHACapMod += item->HeroicCha;
b->MR += CalcItemBonus(item->MR + item->HeroicMR);
b->FR += CalcItemBonus(item->FR + item->HeroicFR);
@@ -340,16 +340,16 @@ void Mob::AddItemBonuses(const EQ::ItemInstance* inst, StatBonuses* b, bool is_a
b->HeroicDR += CalcItemBonus(item->HeroicDR);
b->HeroicCorrup += CalcItemBonus(item->HeroicSVCorrup);
b->MRCapMod += b->HeroicMR;
b->FRCapMod += b->HeroicFR;
b->CRCapMod += b->HeroicCR;
b->PRCapMod += b->HeroicPR;
b->DRCapMod += b->HeroicDR;
b->CorrupCapMod += b->HeroicCorrup;
b->MRCapMod += item->HeroicMR;
b->FRCapMod += item->HeroicFR;
b->CRCapMod += item->HeroicCR;
b->PRCapMod += item->HeroicPR;
b->DRCapMod += item->HeroicDR;
b->CorrupCapMod += item->HeroicSVCorrup;
b->HPRegen += CalcItemBonus(item->Regen);
b->ManaRegen += CalcItemBonus(item->ManaRegen);
b->ManaRegen += CalcItemBonus(item->EnduranceRegen);
b->EnduranceRegen += CalcItemBonus(item->EnduranceRegen);
// These have rule-configured caps.
b->ATK = CalcCappedItemBonus(b->ATK, item->Attack, RuleI(Character, ItemATKCap) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap);
+91 -121
View File
@@ -2256,7 +2256,7 @@ void Bot::AI_Process()
SetAttackingFlag(false);
}
float tar_distance = DistanceSquared(m_Position, tar->GetPosition());
float tar_distance = DistanceSquaredNoZ(m_Position, tar->GetPosition());
// TARGET VALIDATION
if (!IsValidTarget(bot_owner, leash_owner, lo_distance, leash_distance, tar, tar_distance)) {
@@ -2296,7 +2296,6 @@ void Bot::AI_Process()
CombatRangeInput input = {
.target = tar,
.target_distance = tar_distance,
.behind_mob = behind_mob,
.stop_melee_level = stop_melee_level,
.p_item = p_item,
.s_item = s_item
@@ -2368,6 +2367,16 @@ void Bot::AI_Process()
return;
}
if (
HasTargetReflection() &&
!IsTaunting() &&
!tar->IsFleeing() &&
!tar->IsFeared() &&
TryEvade(tar)
) {
return;
}
// ENGAGED AT COMBAT RANGE
// We can fight
@@ -2434,7 +2443,11 @@ void Bot::AI_Process()
ranged_timer.Start();
}
else if (!IsBotRanged() && GetLevel() < stop_melee_level) {
if (!GetMaxMeleeRange() || !RuleB(Bots, DisableSpecialAbilitiesAtMaxMelee)) {
if (
IsTaunting() ||
!GetMaxMeleeRange() ||
!RuleB(Bots, DisableSpecialAbilitiesAtMaxMelee)
) {
DoClassAttacks(tar);
}
@@ -3065,7 +3078,7 @@ CombatRangeOutput Bot::EvaluateCombatRange(const CombatRangeInput& input) {
// For races with a fixed size
if (GetRace() == Race::LavaDragon || GetRace() == Race::Wurm || GetRace() == Race::GhostDragon) {
// size_mod = 60.0f;
size_mod = 60.0f;
}
else if (size_mod < 6.0f) {
size_mod = 8.0f;
@@ -3110,91 +3123,39 @@ CombatRangeOutput Bot::EvaluateCombatRange(const CombatRangeInput& input) {
size_mod = (size_mod / 7.0f);
}
o.melee_distance_max = size_mod;
o.melee_distance_max = sqrt(size_mod);
if (!RuleB(Bots, UseFlatNormalMeleeRange)) {
bool is_two_hander = input.p_item && input.p_item->GetItem()->IsType2HWeapon();
bool is_shield = input.s_item && input.s_item->GetItem()->IsTypeShield();
bool is_backstab_weapon = input.p_item && input.p_item->GetItemBackstabDamage();
bool is_stop_melee_level = GetLevel() >= input.stop_melee_level;
bool is_two_hander = input.p_item && input.p_item->GetItem()->IsType2HWeapon();
bool is_shield = input.s_item && input.s_item->GetItem()->IsTypeShield();
bool is_backstab_weapon = input.p_item && input.p_item->GetItemBackstabDamage();
switch (GetClass()) {
case Class::Warrior:
case Class::Paladin:
case Class::ShadowKnight:
o.melee_distance = (
is_two_hander ? o.melee_distance_max * 0.45f
: is_shield ? o.melee_distance_max * 0.35f
: o.melee_distance_max * 0.40f
);
break;
case Class::Necromancer:
case Class::Wizard:
case Class::Magician:
case Class::Enchanter:
o.melee_distance = (
is_two_hander ? o.melee_distance_max * 0.95f
: o.melee_distance_max * 0.75f
);
break;
case Class::Rogue:
o.melee_distance = (
input.behind_mob && is_backstab_weapon
? o.melee_distance_max * 0.35f
: o.melee_distance_max * 0.50f
);
break;
default:
o.melee_distance = (
is_two_hander ? o.melee_distance_max * 0.70f
: o.melee_distance_max * 0.50f
);
break;
}
o.melee_distance = sqrt(o.melee_distance);
o.melee_distance_max = sqrt(o.melee_distance_max);
if (IsTaunting()) { // Taunting bots
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerTauntingMeleeDistanceMultiplier);
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperTauntingMeleeDistanceMultiplier);
}
else {
o.melee_distance_max = sqrt(o.melee_distance_max);
o.melee_distance = o.melee_distance_max * RuleR(Bots, NormalMeleeRangeDistance);
else if (IsBotRanged()) { // Archers/Throwers
float min_distance = RuleI(Combat, MinRangedAttackDist);
float max_distance = GetBotRangedValue();
float desired_range = GetBotDistanceRanged();
max_distance = (max_distance == 0 ? desired_range : max_distance); // stay ranged even if items/ammo aren't correct
o.melee_distance_min = std::max(min_distance, (desired_range / 2));
o.melee_distance = std::min(max_distance, desired_range);
}
else if (is_stop_melee_level) { // Casters
float desired_range = GetBotDistanceRanged();
if (o.melee_distance > RuleR(Bots, MaxDistanceForMelee)) {
o.melee_distance = RuleR(Bots, MaxDistanceForMelee);
o.melee_distance_min = std::max(o.melee_distance_max, (desired_range / 2));
o.melee_distance = std::max((o.melee_distance_max * 1.25f), desired_range);
}
o.melee_distance_min = o.melee_distance * RuleR(Bots, PercentMinMeleeDistance);
if (IsTaunting()) {
o.melee_distance_min = o.melee_distance * RuleR(Bots, PercentTauntMinMeleeDistance);
o.melee_distance = o.melee_distance * RuleR(Bots, TauntNormalMeleeRangeDistance);
else if (GetMaxMeleeRange()) { // Melee bots set to max melee range
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerMaxMeleeRangeDistanceMultiplier);
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperMaxMeleeRangeDistanceMultiplier);
}
bool is_stop_melee_level = GetLevel() >= input.stop_melee_level;
if (!IsTaunting() && !IsBotRanged() && !is_stop_melee_level && GetMaxMeleeRange()) {
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, PercentMinMaxMeleeRangeDistance);
o.melee_distance = o.melee_distance_max * RuleR(Bots, PercentMaxMeleeRangeDistance);
}
if (is_stop_melee_level && !IsBotRanged()) {
float desired_range = GetBotDistanceRanged();
o.melee_distance_min = std::max(o.melee_distance, (desired_range / 2));
o.melee_distance = std::max((o.melee_distance + 1), desired_range);
}
if (IsBotRanged()) {
float min_distance = RuleI(Combat, MinRangedAttackDist);
float max_distance = GetBotRangedValue();
float desired_range = GetBotDistanceRanged();
max_distance = (max_distance == 0 ? desired_range : max_distance); // stay ranged if set to ranged even if items/ammo aren't correct
o.melee_distance_min = std::max(min_distance, (desired_range / 2));
o.melee_distance = std::min(max_distance, desired_range);
else { // Regular melee
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerMeleeDistanceMultiplier);
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperMeleeDistanceMultiplier);
}
o.at_combat_range = (input.target_distance <= o.melee_distance);
@@ -3615,6 +3576,7 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) {
void Bot::Depop() {
WipeHateList();
entity_list.RemoveFromHateLists(this);
RemoveAllAuras();
if (HasPet())
GetPet()->Depop();
@@ -3740,8 +3702,6 @@ bool Bot::Spawn(Client* botCharacterOwner) {
}
}
MapSpellTypeLevels();
if (RuleB(Bots, RunSpellTypeChecksOnSpawn)) {
OwnerMessage("Running SpellType checks. There may be some spells that are mislabeled as incorrect. Use this as a loose guideline.");
CheckBotSpells(); //This runs through a series of checks and outputs any spells that are set to the wrong spell type in the database
@@ -9766,7 +9726,7 @@ bool Bot::CastChecks(uint16 spell_id, Mob* tar, uint16 spell_type, bool precheck
)
)
&&
tar->CanBuffStack(spell_id, GetLevel(), true) < 0
tar->CanBuffStack(spell_id, GetLevel(), false) < 0
) {
LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanBuffStack.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName());
return false;
@@ -11847,21 +11807,19 @@ void Bot::DoCombatPositioning(
bool front_mob
) {
if (HasTargetReflection()) {
if (!IsTaunting() && !tar->IsFeared() && !tar->IsStunned()) {
if (TryEvade(tar)) {
return;
}
}
else if (tar->IsRooted() && !IsTaunting()) { // Move non-taunters out of range - Above already checks if bot is targeted, otherwise they would stay
if (tar->IsRooted() && !IsTaunting()) { // Move non-taunters out of range
if (tar_distance <= melee_distance_max) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, (melee_distance_max + 1), (melee_distance_max * 2), GetBehindMob(), false)) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, (melee_distance_max + 1), (melee_distance_max * 1.25f), GetBehindMob(), false)) {
RunToGoalWithJitter(Goal);
return;
}
}
}
else if (tar_distance < melee_distance_min || (!front_mob && IsTaunting())) { // Back up any bots that are too close
else if (
tar_distance < melee_distance_min ||
(!front_mob && IsTaunting())
) { // Back up any bots that are too close or if they're taunting and not in front of the mob
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, GetBehindMob(), (IsTaunting() || !GetBehindMob()))) {
RunToGoalWithJitter(Goal);
@@ -11871,46 +11829,58 @@ void Bot::DoCombatPositioning(
}
else {
if (!tar->IsFeared()) {
if (IsTaunting()) { // Taunting adjustments
Mob* mob_tar = tar->GetTarget();
if (!mob_tar) {
DoFaceCheckNoJitter(tar);
return;
}
if (RuleB(Bots, TauntingBotsFollowTopHate)) { // If enabled, taunting bots will stick to top hate
if (Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate)) {
Goal = mob_tar->GetPosition();
RunToGoalWithJitter(Goal);
return;
}
}
else { // Otherwise, stick to any other bots that are taunting
if (mob_tar->IsBot() && mob_tar->CastToBot()->IsTaunting() && (Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate))) {
Goal = mob_tar->GetPosition();
RunToGoalWithJitter(Goal);
return;
}
}
}
else if (tar_distance < melee_distance_min || (GetBehindMob() && !behind_mob) || (IsTaunting() && !front_mob) || !HasRequiredLoSForPositioning(tar)) { // Regular adjustment
if (
tar_distance < melee_distance_min ||
(GetBehindMob() && !behind_mob) ||
(IsTaunting() && !front_mob) ||
!HasRequiredLoSForPositioning(tar)
) { // Regular adjustment
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, GetBehindMob(), (IsTaunting() || !GetBehindMob()))) {
RunToGoalWithJitter(Goal);
return;
}
}
else if (tar->IsEnraged() && !IsTaunting() && !stop_melee_level && !behind_mob) { // Move non-taunting melee bots behind target during enrage
else if (
tar->IsEnraged() &&
!IsTaunting() &&
!stop_melee_level &&
!behind_mob
) { // Move non-taunting melee bots behind target during enrage
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, true)) {
RunToGoalWithJitter(Goal);
return;
}
}
if (IsTaunting()) { // Taunting adjustments
Mob* mob_tar = tar->GetTarget();
if (mob_tar) {
if (
RuleB(Bots, TauntingBotsFollowTopHate) &&
(Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate))
) { // If enabled, taunting bots will stick to top hate
Goal = mob_tar->GetPosition();
RunToGoalWithJitter(Goal);
return;
}
else { // Otherwise, stick to any other bots that are taunting
if (
mob_tar->IsBot() &&
mob_tar->CastToBot()->IsTaunting() &&
(Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate))
) {
Goal = mob_tar->GetPosition();
RunToGoalWithJitter(Goal);
return;
}
}
}
}
}
}
+1 -6
View File
@@ -235,7 +235,6 @@ static std::map<uint16, std::string> botSubType_names = {
struct CombatRangeInput {
Mob* target;
float target_distance;
bool behind_mob;
uint8 stop_melee_level;
const EQ::ItemInstance* p_item;
const EQ::ItemInstance* s_item;
@@ -685,11 +684,9 @@ public:
// Spell lists
void CheckBotSpells();
void MapSpellTypeLevels();
const std::map<int32_t, std::map<int32_t, BotSpellTypesByClass>>& GetCommandedSpellTypesMinLevels() { return commanded_spells_min_level; }
std::list<BotSpellTypeOrder> GetSpellTypesPrioritized(uint8 priority_type);
static uint16 GetParentSpellType(uint16 spell_type);
bool IsValidSpellTypeBySpellID(uint16 spell_type, uint16 spell_id);
static bool IsValidSpellTypeBySpellID(uint16 spell_type, uint16 spell_id);
inline uint16 GetCastedSpellType() const { return _castedSpellType; }
void SetCastedSpellType(uint16 spell_type);
bool IsValidSpellTypeSubType(uint16 spell_type, uint16 sub_type, uint16 spell_id);
@@ -1153,8 +1150,6 @@ protected:
std::vector<BotSpells> AIBot_spells_enforced;
std::unordered_map<uint16, std::vector<BotSpells_wIndex>> AIBot_spells_by_type;
std::map<int32_t, std::map<int32_t, BotSpellTypesByClass>> commanded_spells_min_level;
std::vector<BotTimer> bot_timers;
std::vector<BotBlockedBuffs> bot_blocked_buffs;
+5 -2
View File
@@ -57,6 +57,7 @@
#include "water_map.h"
#include "worldserver.h"
#include "mob.h"
#include "bot_database.h"
#include <fmt/format.h>
@@ -219,6 +220,8 @@ int bot_command_init(void)
std::vector<std::pair<std::string, uint8>> injected_bot_command_settings;
std::vector<std::string> orphaned_bot_command_settings;
database.botdb.MapCommandedSpellTypeMinLevels();
for (auto bcs_iter : bot_command_settings) {
auto bcl_iter = bot_command_list.find(bcs_iter.first);
@@ -796,10 +799,10 @@ void helper_send_usage_required_bots(Client *bot_owner, uint16 spell_type)
}
}
auto& spell_map = bot->GetCommandedSpellTypesMinLevels();
auto spell_map = database.botdb.GetCommandedSpellTypesMinLevels();
if (spell_map.empty()) {
bot_owner->Message(Chat::Yellow, "No bots are capable of casting this spell type");
bot_owner->Message(Chat::Yellow, "No bots are capable of casting this spell type.");
return;
}
+70
View File
@@ -36,6 +36,7 @@
#include "../common/repositories/bot_pet_buffs_repository.h"
#include "../common/repositories/bot_pet_inventories_repository.h"
#include "../common/repositories/bot_spell_casting_chances_repository.h"
#include "../common/repositories/bot_spells_entries_repository.h"
#include "../common/repositories/bot_settings_repository.h"
#include "../common/repositories/bot_stances_repository.h"
#include "../common/repositories/bot_timers_repository.h"
@@ -2521,3 +2522,72 @@ bool BotDatabase::DeleteBotBlockedBuffs(const uint32 bot_id)
return true;
}
void BotDatabase::MapCommandedSpellTypeMinLevels() {
commanded_spell_type_min_levels.clear();
auto start = std::min({ BotSpellTypes::START, BotSpellTypes::COMMANDED_START, BotSpellTypes::DISCIPLINE_START });
auto end = std::max({ BotSpellTypes::END, BotSpellTypes::COMMANDED_END, BotSpellTypes::DISCIPLINE_END });
for (int i = start; i <= end; ++i) {
if (!Bot::IsValidBotSpellType(i)) {
continue;
}
for (int x = Class::Warrior; x <= Class::Berserker; ++x) {
commanded_spell_type_min_levels[i][x] = {UINT8_MAX, "" };
}
}
auto spell_list = BotSpellsEntriesRepository::All(content_db);
for (const auto& s : spell_list) {
if (!IsValidSpell(s.spell_id)) {
LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id);
continue;
}
auto spell = spells[s.spell_id];
if (spell.target_type == ST_Self) {
continue;
}
int32_t bot_class = s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX;
if (
!EQ::ValueWithin(bot_class, Class::Warrior, Class::Berserker) ||
!Bot::IsValidBotSpellType(s.type)
) {
continue;
}
for (int i = start; i <= end; ++i) {
if (s.minlevel > commanded_spell_type_min_levels[i][bot_class].min_level) {
continue;
}
if (
i > BotSpellTypes::PARENT_TYPE_END &&
i != s.type &&
Bot::GetParentSpellType(i) != s.type
) {
continue;
}
if (!Bot::IsValidSpellTypeBySpellID(i, s.spell_id)) {
continue;
}
if (s.minlevel < commanded_spell_type_min_levels[i][bot_class].min_level) {
commanded_spell_type_min_levels[i][bot_class].min_level = s.minlevel;
commanded_spell_type_min_levels[i][bot_class].description = StringFormat(
"%s [#%u] - Level %u",
GetClassIDName(bot_class),
bot_class,
s.minlevel
);
}
}
}
}
+8 -1
View File
@@ -24,7 +24,7 @@
#include <list>
#include <map>
#include <vector>
#include "bot_structs.h"
class Bot;
class Client;
@@ -130,6 +130,9 @@ public:
bool SaveBotSettings(Mob* m);
bool DeleteBotSettings(const uint32 bot_id);
void MapCommandedSpellTypeMinLevels();
std::map<int32_t, std::map<int32_t, BotSpellTypesByClass>> GetCommandedSpellTypesMinLevels() { return commanded_spell_type_min_levels; }
/* Bot group functions */
bool LoadGroupedBotsByGroupID(const uint32 owner_id, const uint32 group_id, std::list<uint32>& group_list);
@@ -211,6 +214,10 @@ public:
private:
std::string query;
protected:
std::map<int32_t, std::map<int32_t, BotSpellTypesByClass>> commanded_spell_type_min_levels;
};
#endif
-49
View File
@@ -2994,52 +2994,3 @@ void Bot::CheckBotSpells() {
}
}
}
void Bot::MapSpellTypeLevels() {
commanded_spells_min_level.clear();
auto start = std::min({ BotSpellTypes::START, BotSpellTypes::COMMANDED_START, BotSpellTypes::DISCIPLINE_START });
auto end = std::max({ BotSpellTypes::END, BotSpellTypes::COMMANDED_END, BotSpellTypes::DISCIPLINE_END });
for (int i = start; i <= end; ++i) {
if (!Bot::IsValidBotSpellType(i)) {
continue;
}
for (int x = Class::Warrior; x <= Class::Berserker; ++x) {
commanded_spells_min_level[i][x] = { UINT8_MAX, "" };
}
}
auto spell_list = BotSpellsEntriesRepository::All(content_db);
for (const auto& s : spell_list) {
if (!IsValidSpell(s.spell_id)) {
LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id);
continue;
}
uint16_t spell_type = s.type;
int32_t bot_class = s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX;
uint8_t min_level = s.minlevel;
if (
!EQ::ValueWithin(bot_class, Class::Warrior, Class::Berserker) ||
!Bot::IsValidBotSpellType(spell_type)
) {
continue;
}
auto& spell_info = commanded_spells_min_level[spell_type][bot_class];
if (min_level < spell_info.min_level) {
spell_info.min_level = min_level;
spell_info.description = StringFormat(
"%s [#%u]: Level %u",
GetClassIDName(bot_class),
bot_class,
min_level
);
}
}
}
+259
View File
@@ -0,0 +1,259 @@
#include "../../common/http/httplib.h"
#include "../../common/eqemu_logsys.h"
#include "../../common/platform.h"
#include "../zone.h"
#include "../client.h"
#include "../../common/net/eqstream.h"
extern Zone *zone;
void RunTest(const std::string &test_name, const std::string &expected, const std::string &actual)
{
if (expected == actual) {
std::cout << "[✅] " << test_name << " PASSED\n";
} else {
std::cerr << "[❌] " << test_name << " FAILED\n";
std::cerr << " 📌 Expected: " << expected << "\n";
std::cerr << " ❌ Got: " << actual << "\n";
std::exit(1);
}
}
void ZoneCLI::DataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description)
{
if (cmd[{"-h", "--help"}]) {
return;
}
uint32 break_length = 50;
int failed_count = 0;
LogSys.SilenceConsoleLogging();
// boot shell zone for testing
Zone::Bootup(ZoneID("qrg"), 0, false);
zone->StopShutdownTimer();
entity_list.Process();
entity_list.MobProcess();
LogSys.EnableConsoleLogging();
std::cout << "===========================================\n";
std::cout << "\uFE0F> Running DataBuckets Tests...\n";
std::cout << "===========================================\n\n";
Client *client = new Client();
// Basic Key-Value Set/Get
client->DeleteBucket("basic_key");
client->SetBucket("basic_key", "simple_value");
std::string value = client->GetBucket("basic_key");
RunTest("Basic Key-Value Set/Get", "simple_value", value);
// Overwriting a Key
client->SetBucket("basic_key", "new_value");
value = client->GetBucket("basic_key");
RunTest("Overwriting a Key", "new_value", value);
// Deleting a Key
client->DeleteBucket("basic_key");
value = client->GetBucket("basic_key");
RunTest("Deleting a Key", "", value);
// Setting a Key with an Expiration
client->SetBucket("expiring_key", "expires_soon", "S1");
value = client->GetBucket("expiring_key");
RunTest("Setting a Key with an Expiration", "expires_soon", value);
// Ensure Expired Key is Deleted
std::this_thread::sleep_for(std::chrono::seconds(2));
value = client->GetBucket("expiring_key");
RunTest("Ensure Expired Key is Deleted", "", value);
// Cache Read/Write Consistency
client->SetBucket("cache_key", "cached_value");
value = client->GetBucket("cache_key");
RunTest("Cache Read/Write Consistency", "cached_value", value);
// Cache Clears on Key Deletion
client->DeleteBucket("cache_key");
value = client->GetBucket("cache_key");
RunTest("Cache Clears on Key Deletion", "", value);
// Setting a Full JSON String
client->SetBucket("json_key", R"({"key1":"value1","key2":"value2"})");
value = client->GetBucket("json_key");
RunTest("Setting a Full JSON String", R"({"key1":"value1","key2":"value2"})", value);
// Overwriting JSON with a Simple String
client->SetBucket("json_key", "string_value");
value = client->GetBucket("json_key");
RunTest("Overwriting JSON with a Simple String", "string_value", value);
// Deleting Non-Existent Key
client->DeleteBucket("non_existent_key");
value = client->GetBucket("non_existent_key");
RunTest("Deleting Non-Existent Key", "", value);
// Basic Key-Value Storage**
client->DeleteBucket("simple_key"); // Reset
client->SetBucket("simple_key", "simple_value");
value = client->GetBucket("simple_key");
RunTest("Basic Key-Value Set/Get", "simple_value", value);
// Nested Key Storage**
client->DeleteBucket("nested");
client->SetBucket("nested.test1", "value1");
client->SetBucket("nested.test2", "value2");
value = client->GetBucket("nested");
RunTest("Nested Key Set/Get", R"({"test1":"value1","test2":"value2"})", value);
// Prevent Overwriting Objects**
client->DeleteBucket("nested");
client->SetBucket("nested.test1.a", "value1");
client->SetBucket("nested.test2.a", "value2");
client->SetBucket("nested.test2", "new_value"); // Should be **rejected**
value = client->GetBucket("nested");
RunTest("Prevent Overwriting Objects", R"({"test1":{"a":"value1"},"test2":{"a":"value2"}})", value);
// Deleting a Specific Nested Key**
client->DeleteBucket("nested");
client->SetBucket("nested.test1", "value1");
client->SetBucket("nested.test2", "value2");
client->DeleteBucket("nested.test1");
value = client->GetBucket("nested");
RunTest("Delete Nested Key", R"({"test2":"value2"})", value);
// Deleting the Entire Parent Key**
client->DeleteBucket("nested");
value = client->GetBucket("nested");
RunTest("Delete Parent Key", "", value);
// Expiration is Ignored for Nested Keys**
client->DeleteBucket("exp_test");
client->SetBucket("exp_test.nested", "data", "S20"); // Expiration ignored
value = client->GetBucket("exp_test");
RunTest("Expiration Ignored for Nested Keys", R"({"nested":"data"})", value);
// Cache Behavior**
client->DeleteBucket("cache_test");
client->SetBucket("cache_test", "cache_value");
value = client->GetBucket("cache_test");
RunTest("Cache Read/Write Consistency", "cache_value", value);
// Ensure Deleting Parent Key Clears Cache**
client->DeleteBucket("cache_test");
value = client->GetBucket("cache_test");
RunTest("Cache Clears on Parent Delete", "", value);
// Setting an Entire JSON Object**
client->DeleteBucket("full_json");
client->SetBucket("full_json", R"({"key1":"value1","key2":{"subkey":"subvalue"}})");
value = client->GetBucket("full_json");
RunTest("Set and Retrieve Full JSON Structure", R"({"key1":"value1","key2":{"subkey":"subvalue"}})", value);
// Partial Nested Key Deletion within JSON**
client->DeleteBucket("full_json");
client->SetBucket("full_json", R"({"key1":"value1","key2":{"subkey":"subvalue"}})");
client->DeleteBucket("full_json.key2");
value = client->GetBucket("full_json");
RunTest("Delete Nested Key within JSON", R"({"key1":"value1"})", value);
// Ensure Object Protection on Overwrite Attempt**
client->DeleteBucket("complex");
client->SetBucket("complex.nested.obj1", "data1");
client->SetBucket("complex.nested.obj2", "data2");
client->SetBucket("complex.nested", "overwrite_attempt"); // Should be rejected
value = client->GetBucket("complex");
RunTest("Ensure Object Protection on Overwrite Attempt", R"({"nested":{"obj1":"data1","obj2":"data2"}})", value);
// Deleting Non-Existent Key Doesn't Break Existing Data**
client->DeleteBucket("complex");
client->SetBucket("complex.nested.obj1", "data1");
client->SetBucket("complex.nested.obj2", "data2");
client->DeleteBucket("does_not_exist"); // Should do nothing
value = client->GetBucket("complex");
RunTest("Deleting Non-Existent Key Doesn't Break Existing Data", R"({"nested":{"obj1":"data1","obj2":"data2"}})", value);
// Get nested key value one level up **
client->DeleteBucket("complex");
client->SetBucket("complex.nested.obj1", "data1");
client->SetBucket("complex.nested.obj2", "data2");
value = client->GetBucket("complex.nested");
RunTest("Get nested key value", R"({"obj1":"data1","obj2":"data2"})", value);
// Get nested key value deep **
client->DeleteBucket("complex");
client->SetBucket("complex.nested.obj1", "data1");
client->SetBucket("complex.nested.obj2", "data2");
value = client->GetBucket("complex.nested.obj2");
RunTest("Get nested key value deep", R"(data2)", value);
// Retrieve Nested Key from Plain String**
client->DeleteBucket("plain_string");
client->SetBucket("plain_string", "some_value");
value = client->GetBucket("plain_string.nested");
RunTest("Retrieve Nested Key from Plain String", "", value);
// Store and Retrieve JSON Array**
client->DeleteBucket("json_array");
client->SetBucket("json_array", R"(["item1", "item2"])");
value = client->GetBucket("json_array");
RunTest("Store and Retrieve JSON Array", R"(["item1", "item2"])", value);
// // Prevent Overwriting Array with Object**
// client->DeleteBucket("json_array");
// client->SetBucket("json_array", R"(["item1", "item2"])");
// client->SetBucket("json_array.item", "new_value"); // Should be rejected
// value = client->GetBucket("json_array");
// RunTest("Prevent Overwriting Array with Object", R"(["item1", "item2"])", value);
// Retrieve Non-Existent Nested Key**
client->DeleteBucket("nested_partial");
client->SetBucket("nested_partial.level1", R"({"exists": "yes"})");
value = client->GetBucket("nested_partial.level1.non_existent");
RunTest("Retrieve Non-Existent Nested Key", "", value);
// Overwriting Parent Key Deletes Children**
client->DeleteBucket("nested_override");
client->SetBucket("nested_override.child", "data");
client->SetBucket("nested_override", "new_parent_value"); // Should remove `child`
value = client->GetBucket("nested_override");
RunTest("Overwriting Parent Key Deletes Children", "new_parent_value", value);
// Store and Retrieve Empty JSON Object**
client->DeleteBucket("empty_json");
client->SetBucket("empty_json", R"({})");
value = client->GetBucket("empty_json");
RunTest("Store and Retrieve Empty JSON Object", R"({})", value);
// Store and Retrieve JSON String**
client->DeleteBucket("json_string");
client->SetBucket("json_string", R"("this is a string")");
value = client->GetBucket("json_string");
RunTest("Store and Retrieve JSON String", R"("this is a string")", value);
// Deeply Nested Key Retrieval**
client->DeleteBucket("deep_nested");
client->SetBucket("deep_nested.level1.level2.level3.level4.level5", "final_value");
value = client->GetBucket("deep_nested.level1.level2.level3.level4.level5");
RunTest("Deeply Nested Key Retrieval", "final_value", value);
// Setting a Key with an Expiration
client->SetBucket("nested_expire.test.test", "shouldnt_expire", "S1");
value = client->GetBucket("nested_expire");
std::this_thread::sleep_for(std::chrono::seconds(2));
RunTest("Setting a nested key with an expiration protection test", R"({"test":{"test":"shouldnt_expire"}})", value);
// Delete Deep Nested Key Keeps Parent**
// client->DeleteBucket("deep_nested");
// client->SetBucket("deep_nested.level1.level2.level3", R"({"key": "value"})");
// client->DeleteBucket("deep_nested.level1.level2.level3.key");
// value = client->GetBucket("deep_nested.level1.level2.level3");
// RunTest("Delete Deep Nested Key Keeps Parent", "{}", value);
std::cout << "\n===========================================\n";
std::cout << "✅ All DataBucket Tests Completed!\n";
std::cout << "===========================================\n";
}
+141 -110
View File
@@ -4,8 +4,76 @@
#include "../zone.h"
#include "../client.h"
#include "../../common/net/eqstream.h"
#include "../../common/json/json.hpp"
extern Zone *zone;
using json = nlohmann::json;
struct HandinEntry {
std::string item_id = "0";
uint32 count = 0;
const EQ::ItemInstance *item = nullptr;
bool is_multiquest_item = false; // state
};
struct HandinMoney {
uint32 platinum = 0;
uint32 gold = 0;
uint32 silver = 0;
uint32 copper = 0;
};
struct Handin {
std::vector<HandinEntry> items = {}; // items can be removed from this set as successful handins are made
HandinMoney money = {}; // money can be removed from this set as successful handins are made
};
struct TestCase {
std::string description;
Handin hand_in;
Handin required;
Handin returned;
bool handin_check_result;
};
void RunTest(const std::string &test_name, bool expected, bool actual)
{
if (expected == actual) {
std::cout << "[✅] " << test_name << " PASSED\n";
}
else {
std::cerr << "[❌] " << test_name << " FAILED\n";
std::cerr << " 📌 Expected: " << (expected ? "true" : "false") << "\n";
std::cerr << " ❌ Got: " << (actual ? "true" : "false") << "\n";
std::exit(1);
}
}
void RunSerializedTest(const std::string &test_name, const std::string &expected, const std::string &actual)
{
if (expected == actual) {
std::cout << "[✅] " << test_name << " PASSED\n";
}
else {
std::cerr << "[❌] " << test_name << " FAILED\n";
std::cerr << " 📌 Expected: " << expected << "\n";
std::cerr << " ❌ Got: " << actual << "\n";
std::exit(1);
}
}
std::string SerializeHandin(const std::map<std::string, uint32> &items, const HandinMoney &money)
{
json j;
j["items"] = items;
j["money"] = {
{"platinum", money.platinum},
{"gold", money.gold},
{"silver", money.silver},
{"copper", money.copper}
};
return j.dump();
}
void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description)
{
@@ -13,15 +81,6 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
return;
}
uint32 break_length = 50;
int failed_count = 0;
RegisterExecutablePlatform(EQEmuExePlatform::ExePlatformZoneSidecar);
LogInfo("{}", Strings::Repeat("-", break_length));
LogInfo("Booting test zone for NPC handins");
LogInfo("{}", Strings::Repeat("-", break_length));
LogSys.SilenceConsoleLogging();
Zone::Bootup(ZoneID("qrg"), 0, false);
@@ -30,9 +89,9 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
entity_list.Process();
entity_list.MobProcess();
LogInfo("{}", Strings::Repeat("-", break_length));
LogInfo("> Done booting test zone");
LogInfo("{}", Strings::Repeat("-", break_length));
std::cout << "===========================================\n";
std::cout << "\uFE0F> Running Hand-in Tests...\n";
std::cout << "===========================================\n\n";
Client *c = new Client();
auto npc_type = content_db.LoadNPCTypesData(754008);
@@ -46,36 +105,6 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
entity_list.AddNPC(npc);
LogInfo("> Spawned NPC [{}]", npc->GetCleanName());
LogInfo("> Spawned client [{}]", c->GetCleanName());
struct HandinEntry {
std::string item_id = "0";
uint32 count = 0;
const EQ::ItemInstance *item = nullptr;
bool is_multiquest_item = false; // state
};
struct HandinMoney {
uint32 platinum = 0;
uint32 gold = 0;
uint32 silver = 0;
uint32 copper = 0;
};
struct Handin {
std::vector<HandinEntry> items = {}; // items can be removed from this set as successful handins are made
HandinMoney money = {}; // money can be removed from this set as successful handins are made
};
struct TestCase {
std::string description = "";
Handin hand_in;
Handin required;
Handin returned;
bool handin_check_result;
};
std::vector<TestCase> test_cases = {
TestCase{
.description = "Test basic cloth-cap hand-in",
@@ -155,7 +184,10 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
.items = {},
.money = {.platinum = 100},
},
.returned = {},
.returned = {
.items = {},
.money = {.platinum = 1},
},
.handin_check_result = false,
},
TestCase{
@@ -168,7 +200,10 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
.items = {},
.money = {.platinum = 100, .gold = 100, .silver = 100, .copper = 100},
},
.returned = {},
.returned = {
.items = {},
.money = {.platinum = 1, .gold = 1, .silver = 1, .copper = 1},
},
.handin_check_result = false,
},
TestCase{
@@ -217,8 +252,11 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
},
.returned = {
.items = {
HandinEntry{.item_id = "1001", .count = 1},
HandinEntry{
.item_id = "1001", .count = 0,
},
},
.money = {.platinum = 1},
},
.handin_check_result = false,
},
@@ -304,12 +342,7 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
HandinEntry{.item_id = "1007", .count = 1},
HandinEntry{.item_id = "1007", .count = 1},
},
.money = {
.platinum = 1,
.gold = 666,
.silver = 234,
.copper = 444,
},
.money = {},
},
.handin_check_result = true,
},
@@ -402,8 +435,8 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
},
};
std::map<std::string, uint32> hand_ins;
std::map<std::string, uint32> required;
std::map<std::string, uint32> hand_ins;
std::map<std::string, uint32> required;
std::vector<EQ::ItemInstance *> items;
LogSys.EnableConsoleLogging();
@@ -411,14 +444,12 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
// turn this on to see debugging output
LogSys.log_settings[Logs::NpcHandin].log_to_console = std::getenv("DEBUG") ? 3 : 0;
LogInfo("{}", Strings::Repeat("-", break_length));
for (auto &test_case: test_cases) {
for (auto &test: test_cases) {
hand_ins.clear();
required.clear();
items.clear();
for (auto &hand_in: test_case.hand_in.items) {
for (auto &hand_in: test.hand_in.items) {
auto item_id = Strings::ToInt(hand_in.item_id);
EQ::ItemInstance *inst = database.CreateItem(item_id);
if (inst->IsStackable()) {
@@ -434,72 +465,76 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
}
// money
if (test_case.hand_in.money.platinum > 0) {
hand_ins["platinum"] = test_case.hand_in.money.platinum;
if (test.hand_in.money.platinum > 0) {
hand_ins["platinum"] = test.hand_in.money.platinum;
}
if (test_case.hand_in.money.gold > 0) {
hand_ins["gold"] = test_case.hand_in.money.gold;
if (test.hand_in.money.gold > 0) {
hand_ins["gold"] = test.hand_in.money.gold;
}
if (test_case.hand_in.money.silver > 0) {
hand_ins["silver"] = test_case.hand_in.money.silver;
if (test.hand_in.money.silver > 0) {
hand_ins["silver"] = test.hand_in.money.silver;
}
if (test_case.hand_in.money.copper > 0) {
hand_ins["copper"] = test_case.hand_in.money.copper;
if (test.hand_in.money.copper > 0) {
hand_ins["copper"] = test.hand_in.money.copper;
}
for (auto &req: test_case.required.items) {
for (auto &req: test.required.items) {
required[req.item_id] = req.count;
}
// money
if (test_case.required.money.platinum > 0) {
required["platinum"] = test_case.required.money.platinum;
if (test.required.money.platinum > 0) {
required["platinum"] = test.required.money.platinum;
}
if (test_case.required.money.gold > 0) {
required["gold"] = test_case.required.money.gold;
if (test.required.money.gold > 0) {
required["gold"] = test.required.money.gold;
}
if (test_case.required.money.silver > 0) {
required["silver"] = test_case.required.money.silver;
if (test.required.money.silver > 0) {
required["silver"] = test.required.money.silver;
}
if (test_case.required.money.copper > 0) {
required["copper"] = test_case.required.money.copper;
if (test.required.money.copper > 0) {
required["copper"] = test.required.money.copper;
}
auto result = npc->CheckHandin(c, hand_ins, required, items);
if (result != test_case.handin_check_result) {
failed_count++;
LogError("FAIL [{}]", test_case.description);
// print out the hand-ins
LogError("Hand-ins >");
for (auto &hand_in: hand_ins) {
LogError(" > Item [{}] count [{}]", hand_in.first, hand_in.second);
}
LogError("Required >");
for (auto &req: required) {
LogError(" > Item [{}] count [{}]", req.first, req.second);
}
LogError("Expected [{}] got [{}]", test_case.handin_check_result, result);
}
else {
LogInfo("PASS [{}]", test_case.description);
}
RunTest(test.description, test.handin_check_result, result);
auto returned = npc->ReturnHandinItems(c);
// assert that returned items are expected
for (auto &item: test_case.returned.items) {
auto found = false;
for (auto &ret: returned.items) {
if (ret.item_id == item.item_id) {
found = true;
break;
}
}
if (!found) {
LogError("Returned item [{}] not expected", item.item_id);
}
std::map<std::string, uint32> returned_items;
HandinMoney returned_money{};
// Serialize returned items
for (const auto &ret: returned.items) {
// if (ret.item->IsStackable() && ret.item->GetCharges() != ret.count) {
// ret.item->SetCharges(ret.count);
// }
returned_items[ret.item_id] += ret.count;
}
// Serialize returned money
returned_money.platinum = returned.money.platinum;
returned_money.gold = returned.money.gold;
returned_money.silver = returned.money.silver;
returned_money.copper = returned.money.copper;
// Serialize expected and actual return values for comparison
std::map<std::string, uint32> expected_returned_items;
for (const auto &entry: test.returned.items) {
expected_returned_items[entry.item_id] += entry.count;
}
std::string expected_serialized = SerializeHandin(
expected_returned_items,
test.returned.money
);
std::string actual_serialized = SerializeHandin(returned_items, returned_money);
// Run serialization check test
RunSerializedTest(test.description + " (Return Validation)", expected_serialized, actual_serialized);
npc->ResetHandin();
if (LogSys.log_settings[Logs::NpcHandin].log_to_console > 0) {
@@ -508,11 +543,7 @@ void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &
}
}
if (failed_count > 0) {
LogError("Failed [{}] tests", failed_count);
std::exit(1);
}
else {
LogInfo("All tests passed");
}
std::cout << "\n===========================================\n";
std::cout << "✅ All NPC Hand-in Tests Completed!\n";
std::cout << "===========================================\n";
}
+27 -71
View File
@@ -16,12 +16,6 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
uint32 break_length = 50;
int failed_count = 0;
RegisterExecutablePlatform(EQEmuExePlatform::ExePlatformZoneSidecar);
LogInfo("{}", Strings::Repeat("-", break_length));
LogInfo("Booting test zone for NPC handins (MultiQuest)");
LogInfo("{}", Strings::Repeat("-", break_length));
LogSys.SilenceConsoleLogging();
Zone::Bootup(ZoneID("qrg"), 0, false);
@@ -30,9 +24,9 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
entity_list.Process();
entity_list.MobProcess();
LogInfo("{}", Strings::Repeat("-", break_length));
LogInfo("> Done booting test zone");
LogInfo("{}", Strings::Repeat("-", break_length));
std::cout << "===========================================\n";
std::cout << "\uFE0F> Running Hand-in Tests (Multi-Quest)...\n";
std::cout << "===========================================\n\n";
Client *c = new Client();
auto npc_type = content_db.LoadNPCTypesData(754008);
@@ -47,9 +41,6 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
entity_list.AddNPC(npc);
npc->MultiQuestEnable();
LogInfo("> Spawned NPC [{}]", npc->GetCleanName());
LogInfo("> Spawned client [{}]", c->GetCleanName());
struct HandinEntry {
std::string item_id = "0";
uint32 count = 0;
@@ -108,12 +99,10 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
// turn this on to see debugging output
LogSys.log_settings[Logs::NpcHandin].log_to_console = std::getenv("DEBUG") ? 3 : 0;
LogInfo("{}", Strings::Repeat("-", break_length));
for (auto &test_case: test_cases) {
for (auto &test: test_cases) {
required.clear();
for (auto &hand_in: test_case.hand_in.items) {
for (auto &hand_in: test.hand_in.items) {
hand_ins.clear();
items.clear();
@@ -135,72 +124,43 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
}
// money
if (test_case.hand_in.money.platinum > 0) {
hand_ins["platinum"] = test_case.hand_in.money.platinum;
if (test.hand_in.money.platinum > 0) {
hand_ins["platinum"] = test.hand_in.money.platinum;
}
if (test_case.hand_in.money.gold > 0) {
hand_ins["gold"] = test_case.hand_in.money.gold;
if (test.hand_in.money.gold > 0) {
hand_ins["gold"] = test.hand_in.money.gold;
}
if (test_case.hand_in.money.silver > 0) {
hand_ins["silver"] = test_case.hand_in.money.silver;
if (test.hand_in.money.silver > 0) {
hand_ins["silver"] = test.hand_in.money.silver;
}
if (test_case.hand_in.money.copper > 0) {
hand_ins["copper"] = test_case.hand_in.money.copper;
if (test.hand_in.money.copper > 0) {
hand_ins["copper"] = test.hand_in.money.copper;
}
for (auto &req: test_case.required.items) {
for (auto &req: test.required.items) {
required[req.item_id] = req.count;
}
// money
if (test_case.required.money.platinum > 0) {
required["platinum"] = test_case.required.money.platinum;
if (test.required.money.platinum > 0) {
required["platinum"] = test.required.money.platinum;
}
if (test_case.required.money.gold > 0) {
required["gold"] = test_case.required.money.gold;
if (test.required.money.gold > 0) {
required["gold"] = test.required.money.gold;
}
if (test_case.required.money.silver > 0) {
required["silver"] = test_case.required.money.silver;
if (test.required.money.silver > 0) {
required["silver"] = test.required.money.silver;
}
if (test_case.required.money.copper > 0) {
required["copper"] = test_case.required.money.copper;
if (test.required.money.copper > 0) {
required["copper"] = test.required.money.copper;
}
auto result = npc->CheckHandin(c, hand_ins, required, items);
if (result != test_case.handin_check_result) {
failed_count++;
LogError("FAIL [{}]", test_case.description);
// print out the hand-ins
LogError("Hand-ins >");
for (auto &item: npc->GetHandin().items) {
LogError(" > Item [{}] count [{}]", item.item_id, item.count);
}
LogError("Required >");
for (auto &req: required) {
LogError(" > Item [{}] count [{}]", req.first, req.second);
}
LogError("Expected [{}] got [{}]", test_case.handin_check_result, result);
}
else {
LogInfo("PASS [{}]", test_case.description);
}
RunTest(test.description, test.handin_check_result, result);
auto returned = npc->ReturnHandinItems(c);
// assert that returned items are expected
for (auto &item: test_case.returned.items) {
auto found = false;
for (auto &ret: returned.items) {
if (ret.item_id == item.item_id) {
found = true;
break;
}
}
if (!found) {
LogError("Returned item [{}] not expected", item.item_id);
}
}
npc->ResetHandin();
if (LogSys.log_settings[Logs::NpcHandin].log_to_console > 0) {
@@ -209,11 +169,7 @@ void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std
}
}
if (failed_count > 0) {
LogError("Failed [{}] tests", failed_count);
std::exit(1);
}
else {
LogInfo("All tests passed");
}
std::cout << "\n===========================================\n";
std::cout << "✅ All NPC Hand-in Tests Completed (Multi-Quest)!\n";
std::cout << "===========================================\n";
}
+3
View File
@@ -708,6 +708,9 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
}
Client::~Client() {
entity_list.RemoveMobFromCloseLists(this);
m_close_mobs.clear();
if (ClientVersion() == EQ::versions::ClientVersion::RoF2 && RuleB (Parcel, EnableParcelMerchants)) {
DoParcelCancel();
}
+2 -1
View File
@@ -404,6 +404,7 @@ public:
void LoadParcels();
std::map<uint32, CharacterParcelsRepository::CharacterParcels> GetParcels() { return m_parcels; }
int32 FindNextFreeParcelSlot(uint32 char_id);
int32 FindNextFreeParcelSlotUsingMemory();
void SendParcelIconStatus();
void SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action);
@@ -1900,7 +1901,7 @@ public:
void SendEvolvingPacket(int8 action, const CharacterEvolvingItemsRepository::CharacterEvolvingItems &item);
void DoEvolveItemToggle(const EQApplicationPacket* app);
void DoEvolveItemDisplayFinalResult(const EQApplicationPacket* app);
bool DoEvolveCheckProgression(const EQ::ItemInstance &inst);
bool DoEvolveCheckProgression(EQ::ItemInstance &inst);
void SendEvolveXPWindowDetails(const EQApplicationPacket* app);
void DoEvolveTransferXP(const EQApplicationPacket* app);
void SendEvolveXPTransferWindow();
+45 -2
View File
@@ -71,7 +71,7 @@ void Client::SendEvolvingPacket(const int8 action, const CharacterEvolvingItemsR
void Client::ProcessEvolvingItem(const uint64 exp, const Mob *mob)
{
std::vector<const EQ::ItemInstance *> queue{};
std::vector<EQ::ItemInstance *> queue{};
for (auto &[key, inst]: GetInv().GetWorn()) {
LogEvolveItemDetail(
@@ -128,6 +128,7 @@ void Client::ProcessEvolvingItem(const uint64 exp, const Mob *mob)
evolve_amount = exp * RuleR(EvolvingItems, PercentOfSoloExperience) / 100;
}
inst->SetEvolveAddToCurrentAmount(evolve_amount);
inst->CalculateEvolveProgression();
auto e = CharacterEvolvingItemsRepository::SetCurrentAmountAndProgression(
@@ -298,7 +299,7 @@ void Client::DoEvolveItemDisplayFinalResult(const EQApplicationPacket *app)
}
}
bool Client::DoEvolveCheckProgression(const EQ::ItemInstance &inst)
bool Client::DoEvolveCheckProgression(EQ::ItemInstance &inst)
{
if (inst.GetEvolveProgression() < 100 || inst.GetEvolveLvl() == inst.GetMaxEvolveLvl()) {
return false;
@@ -315,6 +316,48 @@ bool Client::DoEvolveCheckProgression(const EQ::ItemInstance &inst)
return false;
}
if (RuleB(EvolvingItems, EnableParcelMerchants) &&
!RuleB(EvolvingItems, DestroyAugmentsOnEvolve) &&
inst.IsAugmented()
) {
auto const augs = inst.GetAugmentIDs();
std::vector<CharacterParcelsRepository::CharacterParcels> parcels;
for (auto const &item_id: augs) {
if (!item_id) {
continue;
}
CharacterParcelsRepository::CharacterParcels p{};
p.char_id = CharacterID();
p.from_name = "Evolving Item Sub-System";
p.note = fmt::format(
"System automatically removed from {} which recently evolved.",
inst.GetItem()->Name
);
p.slot_id = FindNextFreeParcelSlotUsingMemory();
p.sent_date = time(nullptr);
p.item_id = item_id;
p.quantity = 1;
if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
PlayerEvent::ParcelSend e{};
e.from_player_name = p.from_name;
e.to_player_name = GetCleanName();
e.item_id = p.item_id;
e.quantity = 1;
e.sent_date = p.sent_date;
RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e);
}
parcels.push_back(p);
}
CharacterParcelsRepository::InsertMany(database, parcels);
SendParcelStatus();
SendParcelIconStatus();
}
CheckItemDiscoverability(new_inst->GetID());
PlayerEvent::EvolveItem e{};
+1 -1
View File
@@ -16974,7 +16974,7 @@ void Client::Handle_OP_GuildTributeDonateItem(const EQApplicationPacket *app)
SendGuildTributeDonateItemReply(in, favor);
if(player_event_logs.IsEventEnabled(PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM)) {
if(inst && player_event_logs.IsEventEnabled(PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM)) {
auto e = PlayerEvent::GuildTributeDonateItem{ .item_id = inst->GetID(),
.augment_1_id = inst->GetAugmentItemID(0),
.augment_2_id = inst->GetAugmentItemID(1),
+1 -1
View File
@@ -1569,7 +1569,7 @@ void Corpse::LootCorpseItem(Client *c, const EQApplicationPacket *app)
}
}
if (player_event_logs.IsEventEnabled(PlayerEvent::LOOT_ITEM) && !IsPlayerCorpse()) {
if (inst && player_event_logs.IsEventEnabled(PlayerEvent::LOOT_ITEM) && !IsPlayerCorpse()) {
auto e = PlayerEvent::LootItemEvent{
.item_id = inst->GetItem()->ID,
.item_name = inst->GetItem()->Name,
+145 -41
View File
@@ -27,7 +27,8 @@ void DataBucket::SetData(const std::string &bucket_key, const std::string &bucke
void DataBucket::SetData(const DataBucketKey &k_)
{
DataBucketKey k = k_; // copy the key so we can modify it
if (k.key.find(NESTED_KEY_DELIMITER) != std::string::npos) {
bool is_nested = k.key.find(NESTED_KEY_DELIMITER) != std::string::npos;
if (is_nested) {
k.key = Strings::Split(k.key, NESTED_KEY_DELIMITER).front();
}
@@ -63,6 +64,10 @@ void DataBucket::SetData(const DataBucketKey &k_)
if (isalpha(k.expires[0]) || isalpha(k.expires[k.expires.length() - 1])) {
expires_time_unix = static_cast<int64>(std::time(nullptr)) + Strings::TimeToSeconds(k.expires);
}
if (is_nested) {
LogDataBuckets("Nested keys can't expire; set expiration on the parent key");
expires_time_unix = 0;
}
}
b.expires = expires_time_unix;
@@ -75,26 +80,45 @@ void DataBucket::SetData(const DataBucketKey &k_)
std::string existing_value = r.id > 0 ? r.value : "{}";
json json_value = json::object();
try {
json_value = json::parse(existing_value);
} catch (json::parse_error &e) {
LogError("Failed to parse JSON for key [{}]: {}", k_.key, e.what());
json_value = json::object(); // Reset to an empty object on error
// Check if the JSON is valid
if (Strings::IsValidJson(existing_value)) {
try {
json_value = json::parse(existing_value);
} catch (json::parse_error &e) {
LogDataBuckets("Failed to parse JSON for key [{}] [{}]", k_.key, e.what());
json_value = json::object(); // Reset to an empty object on error
}
}
// Recursively merge new key-value pair into the JSON object
auto nested_keys = Strings::Split(k_.key, NESTED_KEY_DELIMITER);
auto top_key = nested_keys.front();
// remove the top-level key
nested_keys.erase(nested_keys.begin());
json *current = &json_value;
for (size_t i = 0; i < nested_keys.size(); ++i) {
const std::string &key_part = nested_keys[i];
if (i == nested_keys.size() - 1) {
LogDataBucketsDetail("Setting key [{}] key_part [{}]", k.key, key_part);
// If the key already exists and is an object or array, prevent overwriting to avoid data loss
if (current->contains(key_part) &&
((*current)[key_part].is_object() || (*current)[key_part].is_array())) {
LogDataBuckets("Attempted to overwrite an existing object or array at key [{}] - skipping", k_.key);
return;
}
// Set the value at the final key
(*current)[key_part] = k_.value;
} else {
// Traverse or create nested objects
if (!current->contains(key_part)) {
(*current)[key_part] = json::object();
LogDataBucketsDetail("Creating nested root key [{}] key_part [{}]", k.key, key_part);
} else if (!(*current)[key_part].is_object()) {
// If key exists but is not an object, reset to object to avoid conflicts
(*current)[key_part] = json::object();
@@ -105,7 +129,7 @@ void DataBucket::SetData(const DataBucketKey &k_)
// Serialize JSON back to string
b.value = json_value.dump();
b.key_ = nested_keys.front(); // Use the top-level key
b.key_ = top_key; // Use the top-level key
}
if (bucket_id) {
@@ -142,12 +166,20 @@ DataBucketsRepository::DataBuckets DataBucket::ExtractNestedValue(
const std::string &full_key)
{
auto nested_keys = Strings::Split(full_key, NESTED_KEY_DELIMITER);
auto top_key = nested_keys.front();
nested_keys.erase(nested_keys.begin());
json json_value;
// Check if the JSON is valid
if (!Strings::IsValidJson(bucket.value)) {
LogDataBuckets("Invalid JSON for key [{}]", bucket.key_);
return DataBucketsRepository::NewEntity();
}
try {
json_value = json::parse(bucket.value); // Parse the JSON
} catch (json::parse_error &ex) {
LogError("Failed to parse JSON for key [{}]: {}", bucket.key_, ex.what());
LogDataBuckets("Failed to parse JSON for key [{}] [{}]", bucket.key_, ex.what());
return DataBucketsRepository::NewEntity(); // Return empty entity on parse error
}
@@ -336,44 +368,116 @@ bool DataBucket::GetDataBuckets(Mob *mob)
bool DataBucket::DeleteData(const DataBucketKey &k)
{
if (CanCache(k)) {
size_t size_before = g_data_bucket_cache.size();
bool is_nested_key = k.key.find(NESTED_KEY_DELIMITER) != std::string::npos;
// delete from cache where contents match
g_data_bucket_cache.erase(
std::remove_if(
g_data_bucket_cache.begin(),
g_data_bucket_cache.end(),
[&](DataBucketsRepository::DataBuckets &e) {
return CheckBucketMatch(e, k);
}
),
g_data_bucket_cache.end()
);
if (!is_nested_key) {
// Update cache
if (CanCache(k)) {
// delete from cache where contents match
g_data_bucket_cache.erase(
std::remove_if(
g_data_bucket_cache.begin(),
g_data_bucket_cache.end(),
[&](DataBucketsRepository::DataBuckets &e) {
return CheckBucketMatch(e, k);
}
),
g_data_bucket_cache.end()
);
}
LogDataBuckets(
"Deleting bucket key [{}] bot_id [{}] account_id [{}] character_id [{}] npc_id [{}] bot_id [{}] zone_id [{}] instance_id [{}] cache size before [{}] after [{}]",
k.key,
k.bot_id,
k.account_id,
k.character_id,
k.npc_id,
k.bot_id,
k.zone_id,
k.instance_id,
size_before,
g_data_bucket_cache.size()
// Regular key deletion, no nesting involved
return DataBucketsRepository::DeleteWhere(
database,
fmt::format("{} `key` = '{}'", DataBucket::GetScopedDbFilters(k), k.key)
);
}
return DataBucketsRepository::DeleteWhere(
database,
fmt::format(
"{} `key` = '{}'",
DataBucket::GetScopedDbFilters(k),
k.key
)
);
// If it's a nested key, retrieve the top-level JSON object
auto top_level_key = Strings::Split(k.key, NESTED_KEY_DELIMITER).front();
DataBucketKey top_level_k = k;
top_level_k.key = top_level_key;
auto r = GetData(top_level_k);
if (r.id == 0 || r.value.empty() || !Strings::IsValidJson(r.value)) {
LogDataBuckets("Attempted to delete nested key [{}] but parent key [{}] does not exist or is invalid JSON", k.key, top_level_key);
return false;
}
json json_value;
try {
json_value = json::parse(r.value);
} catch (json::parse_error &ex) {
LogDataBuckets("Failed to parse JSON for key [{}] [{}]", top_level_key, ex.what());
return false;
}
// Recursively remove the nested key
auto nested_keys = Strings::Split(k.key, NESTED_KEY_DELIMITER);
auto top_key = nested_keys.front();
nested_keys.erase(nested_keys.begin());
json *current = &json_value;
for (size_t i = 0; i < nested_keys.size(); ++i) {
const std::string &key_part = nested_keys[i];
if (i == nested_keys.size() - 1) {
// Last key in the hierarchy - delete it
if (current->contains(key_part)) {
current->erase(key_part);
LogDataBuckets("Deleted nested key [{}] from [{}]", key_part, k.key);
} else {
LogDataBuckets("Key [{}] not found in JSON - nothing to delete", k.key);
return false;
}
} else {
if (!current->contains(key_part) || !(*current)[key_part].is_object()) {
LogDataBuckets("Parent key [{}] does not exist or is not an object", key_part);
return false;
}
current = &(*current)[key_part];
}
}
// If the JSON object is now empty, delete the top-level key
if (json_value.empty()) {
LogDataBuckets("Top-level key [{}] is now empty, deleting entire entry", top_level_key);
// delete cache
if (CanCache(k)) {
g_data_bucket_cache.erase(
std::remove_if(
g_data_bucket_cache.begin(),
g_data_bucket_cache.end(),
[&](DataBucketsRepository::DataBuckets &e) {
return CheckBucketMatch(e, top_level_k);
}
),
g_data_bucket_cache.end()
);
}
return DataBucketsRepository::DeleteWhere(
database,
fmt::format("{} `key` = '{}'", DataBucket::GetScopedDbFilters(k), top_level_key)
);
}
// Otherwise, update the existing JSON without the deleted key
r.value = json_value.dump();
DataBucketsRepository::UpdateOne(database, r);
// Update cache
if (CanCache(k)) {
for (auto &e : g_data_bucket_cache) {
if (CheckBucketMatch(e, top_level_k)) {
e.value = r.value;
break;
}
}
}
return true;
}
std::string DataBucket::GetDataExpires(const DataBucketKey &k)
+44 -1
View File
@@ -25,6 +25,7 @@
#include "worldserver.h"
#include "../common/repositories/character_expedition_lockouts_repository.h"
#include "../common/repositories/dynamic_zone_lockouts_repository.h"
#include <cereal/types/utility.hpp>
extern WorldServer worldserver;
@@ -162,14 +163,28 @@ void DynamicZone::CacheAllFromDatabase()
}
}
dz->UpdateMembers();
zone->dynamic_zone_cache.emplace(dz_id, std::move(dz));
}
if (!zone->dynamic_zone_cache.empty())
{
RequestMemberStatuses();
}
LogInfo("Loaded [{}] dynamic zone(s)", Strings::Commify(zone->dynamic_zone_cache.size()));
LogDynamicZones("Caching [{}] dynamic zone(s) took [{}s]", zone->dynamic_zone_cache.size(), bench.elapsed());
}
void DynamicZone::RequestMemberStatuses()
{
ServerPacket pack(ServerOP_DzGetBulkMemberStatuses, sizeof(ServerDzCerealData_Struct));
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack.pBuffer);
buf->zone_id = static_cast<uint16_t>(zone->GetZoneID());
buf->inst_id = static_cast<uint16_t>(zone->GetInstanceID());
worldserver.SendPacket(&pack);
}
template <typename T>
DynamicZone* FindDynamicZone(T pred)
{
@@ -849,6 +864,34 @@ void DynamicZone::HandleWorldMessage(ServerPacket* pack)
}
break;
}
case ServerOP_DzGetBulkMemberStatuses:
{
if (zone)
{
std::vector<std::pair<uint32_t, std::vector<DynamicZoneMember>>> dzs;
dzs.reserve(zone->dynamic_zone_cache.size());
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack->pBuffer);
EQ::Util::MemoryStreamReader ss(buf->cereal_data, buf->cereal_size);
{
cereal::BinaryInputArchive archive(ss);
archive(dzs);
}
for (const auto& [dz_id, members] : dzs)
{
if (auto dz = DynamicZone::FindDynamicZoneByID(dz_id))
{
for (const auto& member : members)
{
dz->SetInternalMemberStatus(member.id, member.status);
}
dz->m_has_member_statuses = true;
}
}
}
break;
}
case ServerOP_DzUpdateMemberStatus:
{
auto buf = reinterpret_cast<ServerDzMemberStatus_Struct*>(pack->pBuffer);
+2 -1
View File
@@ -92,13 +92,13 @@ public:
void SendMemberNameToZoneMembers(const std::string& char_name, bool remove);
void SendMemberStatusToZoneMembers(const DynamicZoneMember& member);
void SetLocked(bool lock, bool update_db = false, DzLockMsg lock_msg = DzLockMsg::None, uint32_t color = Chat::Yellow);
void UpdateMembers();
std::string GetLootEvent(uint32_t id, DzLootEvent::Type type) const;
void SetLootEvent(uint32_t id, const std::string& event, DzLootEvent::Type type);
private:
static void StartAllClientRemovalTimers();
static void RequestMemberStatuses();
uint16_t GetCurrentInstanceID() const override;
uint16_t GetCurrentZoneID() const override;
@@ -125,6 +125,7 @@ private:
void SendWorldPlayerInvite(const std::string& inviter, const std::string& swap_name, const std::string& add_name, bool pending = false);
void SetUpdatedDuration(uint32_t seconds);
void TryAddClient(Client* add_client, const std::string& inviter, const std::string& swap_name, Client* leader = nullptr);
void UpdateMembers();
std::unique_ptr<EQApplicationPacket> CreateExpireWarningPacket(uint32_t minutes_remaining);
std::unique_ptr<EQApplicationPacket> CreateInfoPacket(bool clear = false);
+3 -3
View File
@@ -2231,7 +2231,7 @@ Raid* EntityList::GetRaidByBotName(const char* name)
}
}
}
return nullptr;
}
@@ -2914,7 +2914,7 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob)
return;
}
if (scanning_mob->GetID() <= 0) {
if (scanning_mob->GetID() <= 0 || scanning_mob->IsZoneController()) {
return;
}
@@ -2933,7 +2933,7 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob)
for (auto &e : mob_list) {
auto mob = e.second;
if (mob->GetID() <= 0) {
if (mob && (mob->GetID() <= 0 || mob->IsZoneController())) {
continue;
}
+2 -2
View File
@@ -197,7 +197,7 @@ void command_parcels(Client *c, const Seperator *sep)
send_to_client.at(0).character_name.c_str()
);
if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
if (inst && player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
PlayerEvent::ParcelSend e{};
e.from_player_name = parcel_out.from_name;
e.to_player_name = send_to_client.at(0).character_name;
@@ -281,7 +281,7 @@ void command_parcels(Client *c, const Seperator *sep)
send_to_client.at(0).character_name.c_str()
);
if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
if (inst && player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
PlayerEvent::ParcelSend e{};
e.from_player_name = parcel_out.from_name;
e.to_player_name = send_to_client.at(0).character_name;
+1 -1
View File
@@ -108,7 +108,7 @@ const NPCType *Horse::BuildHorseType(uint16 spell_id)
n->npc_id = 0;
n->loottable_id = 0;
n->texture = e.texture;
n->helmtexture = e.texture;
n->helmtexture = e.helmtexture == -1 ? e.texture : e.helmtexture;
n->runspeed = e.mountspeed;
n->light = 0;
n->STR = 75;
+2 -2
View File
@@ -763,7 +763,7 @@ void Client::DropItem(int16 slot_id, bool recurse)
int i = 0;
if (player_event_logs.IsEventEnabled(PlayerEvent::DROPPED_ITEM)) {
if (inst && player_event_logs.IsEventEnabled(PlayerEvent::DROPPED_ITEM)) {
auto e = PlayerEvent::DroppedItemEvent{
.item_id = inst->GetID(),
.augment_1_id = inst->GetAugmentItemID(0),
@@ -1655,7 +1655,7 @@ bool Client::SwapItem(MoveItem_Struct* move_in) {
DeleteItemInInventory(EQ::invslot::slotCursor, 0, true);
if (player_event_logs.IsEventEnabled(PlayerEvent::ITEM_DESTROY)) {
if (test_inst && player_event_logs.IsEventEnabled(PlayerEvent::ITEM_DESTROY)) {
auto e = PlayerEvent::DestroyItemEvent{
.item_id = test_inst->GetItem()->ID,
.item_name = test_inst->GetItem()->Name,
+8 -1
View File
@@ -375,6 +375,10 @@ void NPC::AddLootDrop(
if (item2->Slots & slots) {
if (equipment[i]) {
compitem = database.GetItem(equipment[i]);
if (!compitem) {
continue;
}
if (item2->AC > compitem->AC || (item2->AC == compitem->AC && item2->HP > compitem->HP)) {
// item would be an upgrade
// check if we're multi-slot, if yes then we have to keep
@@ -385,6 +389,9 @@ void NPC::AddLootDrop(
else {
// Unequip old item
auto *old_item = GetItem(i);
if (!old_item) {
continue;
}
old_item->equip_slot = EQ::invslot::SLOT_INVALID;
@@ -677,7 +684,7 @@ LootItem *NPC::GetItem(int slot_id)
end = m_loot_items.end();
for (; cur != end; ++cur) {
LootItem *item = *cur;
if (item->equip_slot == slot_id) {
if (item && item->equip_slot == slot_id) {
return item;
}
}
+53 -1
View File
@@ -727,6 +727,52 @@ std::string Lua_Zone::GetBucketRemaining(const std::string& bucket_name)
return self->GetBucketRemaining(bucket_name);
}
void Lua_Zone::ClearVariables()
{
Lua_Safe_Call_Void();
self->ClearVariables();
}
bool Lua_Zone::DeleteVariable(const std::string& variable_name)
{
Lua_Safe_Call_Bool();
return self->DeleteVariable(variable_name);
}
std::string Lua_Zone::GetVariable(const std::string& variable_name)
{
Lua_Safe_Call_String();
return self->GetVariable(variable_name);
}
luabind::object Lua_Zone::GetVariables(lua_State* L)
{
auto t = luabind::newtable(L);
if (d_) {
auto self = reinterpret_cast<NativeType*>(d_);
auto l = self->GetVariables();
int i = 1;
for (const auto& v : l) {
t[i] = v;
i++;
}
}
return t;
}
void Lua_Zone::SetVariable(const std::string& variable_name, const std::string& variable_value)
{
Lua_Safe_Call_Void();
self->SetVariable(variable_name, variable_value);
}
bool Lua_Zone::VariableExists(const std::string& variable_name)
{
Lua_Safe_Call_Bool();
return self->VariableExists(variable_name);
}
luabind::scope lua_register_zone() {
return luabind::class_<Lua_Zone>("Zones")
.def(luabind::constructor<>())
@@ -737,7 +783,9 @@ luabind::scope lua_register_zone() {
.def("CanDoCombat", &Lua_Zone::CanDoCombat)
.def("CanLevitate", &Lua_Zone::CanLevitate)
.def("ClearSpawnTimers", &Lua_Zone::ClearSpawnTimers)
.def("ClearVariables", &Lua_Zone::ClearVariables)
.def("DeleteBucket", (void(Lua_Zone::*)(const std::string&))&Lua_Zone::DeleteBucket)
.def("DeleteVariable", &Lua_Zone::DeleteVariable)
.def("Depop", (void(Lua_Zone::*)(void))&Lua_Zone::Depop)
.def("Depop", (void(Lua_Zone::*)(bool))&Lua_Zone::Depop)
.def("Despawn", &Lua_Zone::Despawn)
@@ -819,6 +867,8 @@ luabind::scope lua_register_zone() {
.def("GetZoneType", &Lua_Zone::GetZoneType)
.def("GetUnderworld", &Lua_Zone::GetUnderworld)
.def("GetUnderworldTeleportIndex", &Lua_Zone::GetUnderworldTeleportIndex)
.def("GetVariable", &Lua_Zone::GetVariable)
.def("GetVariables", &Lua_Zone::GetVariables)
.def("GetWalkSpeed", &Lua_Zone::GetWalkSpeed)
.def("GetZoneZType", &Lua_Zone::GetZoneZType)
.def("GetZoneTotalBlockedSpells", &Lua_Zone::GetZoneTotalBlockedSpells)
@@ -849,6 +899,8 @@ luabind::scope lua_register_zone() {
.def("SetInstanceTimer", &Lua_Zone::SetInstanceTimer)
.def("SetInstanceTimeRemaining", &Lua_Zone::SetInstanceTimeRemaining)
.def("SetIsHotzone", &Lua_Zone::SetIsHotzone)
.def("ShowZoneGlobalLoot", &Lua_Zone::ShowZoneGlobalLoot);
.def("SetVariable", &Lua_Zone::SetVariable)
.def("ShowZoneGlobalLoot", &Lua_Zone::ShowZoneGlobalLoot)
.def("VariableExists", &Lua_Zone::VariableExists);
}
+6
View File
@@ -141,6 +141,12 @@ public:
void SetInstanceTimeRemaining(uint32 time_remaining);
void SetIsHotzone(bool is_hotzone);
void ShowZoneGlobalLoot(Lua_Client c);
void ClearVariables();
bool DeleteVariable(const std::string& variable_name);
std::string GetVariable(const std::string& variable_name);
luabind::object GetVariables(lua_State* L);
void SetVariable(const std::string& variable_name, const std::string& variable_value);
bool VariableExists(const std::string& variable_name);
// data buckets
void SetBucket(const std::string& bucket_name, const std::string& bucket_value);
+1 -1
View File
@@ -306,7 +306,7 @@ int main(int argc, char **argv)
LogSys.SetDatabase(&database)
->SetLogPath(path.GetLogPath())
->LoadLogDatabaseSettings()
->LoadLogDatabaseSettings(ZoneCLI::RanTestCommand(argc, argv))
->SetGMSayHandler(&Zone::GMSayHookCallBackProcess)
->StartFileLogs();
+8 -4
View File
@@ -531,6 +531,9 @@ Mob::Mob(
Mob::~Mob()
{
entity_list.RemoveMobFromCloseLists(this);
m_close_mobs.clear();
quest_manager.stopalltimers(this);
mMovementManager->RemoveMob(this);
@@ -570,11 +573,8 @@ Mob::~Mob()
entity_list.UnMarkNPC(GetID());
UninitializeBuffSlots();
entity_list.RemoveMobFromCloseLists(this);
entity_list.RemoveAuraFromMobs(this);
m_close_mobs.clear();
ClearDataBucketCache();
LeaveHealRotationTargetPool();
@@ -1453,6 +1453,10 @@ void Mob::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho)
ns->spawn.flymode = 0;
}
if (IsZoneController()) {
ns->spawn.invis = 255; // gm invis
}
if (RuleB(Character, AllowCrossClassTrainers) && ForWho) {
if (ns->spawn.class_ >= Class::WarriorGM && ns->spawn.class_ <= Class::BerserkerGM) {
int trainer_class = Class::WarriorGM + (ForWho->GetClass() - 1);
@@ -8347,7 +8351,7 @@ int Mob::DispatchZoneControllerEvent(
RuleB(Zone, UseZoneController) &&
(
!IsNPC() ||
(IsNPC() && GetNPCTypeID() != ZONE_CONTROLLER_NPC_ID)
(IsNPC() && !IsZoneController())
)
) {
auto controller = entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID);
+1
View File
@@ -671,6 +671,7 @@ public:
((static_cast<float>(current_mana) / max_mana) * 100); }
virtual int64 CalcMaxMana();
uint32 GetNPCTypeID() const { return npctype_id; }
inline bool IsZoneController() const { return npctype_id == ZONE_CONTROLLER_NPC_ID; }
void SetNPCTypeID(uint32 npctypeid) { npctype_id = npctypeid; }
inline const glm::vec4& GetPosition() const { return m_Position; }
inline void SetPosition(const float x, const float y, const float z) { m_Position.x = x; m_Position.y = y; m_Position.z = z; }
+5 -4
View File
@@ -941,13 +941,14 @@ bool NPC::SpawnZoneController()
npc_type->findable = 0;
npc_type->trackable = 0;
npc_type->untargetable = 1;
strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1");
strcpy(npc_type->special_abilities, "1,1,3000,50^12,1^14,1^16,1^18,1^19,1^20,1^21,1^22,1^23,1^24,1^25,1^26,1^32,1^33,1^35,1^46,1^47,1^48,1^49,1^50,1^52,1^53,1^54,1^55,1^56,1^57,1");
glm::vec4 point;
point.x = 3000;
point.y = 1000;
point.z = 500;
point.x = 30000;
point.y = 10000;
point.z = -10000;
auto npc = new NPC(npc_type, nullptr, point, GravityBehavior::Flying);
npc->GiveNPCTypeData(npc_type);
+28
View File
@@ -608,6 +608,34 @@ public:
inline void SetResumedFromZoneSuspend(bool state = true) { m_resumed_from_zone_suspend = state; }
inline bool IsResumedFromZoneSuspend() { return m_resumed_from_zone_suspend; }
inline void LoadBuffsFromState(std::vector<Buffs_Struct> in_buffs) {
int i = 0;
for (auto &b: in_buffs) {
buffs[i].spellid = b.spellid;
buffs[i].casterlevel = b.casterlevel;
buffs[i].casterid = b.casterid;
strncpy(buffs[i].caster_name, b.caster_name, 64);
buffs[i].ticsremaining = b.ticsremaining;
buffs[i].counters = b.counters;
buffs[i].hit_number = b.hit_number;
buffs[i].melee_rune = b.melee_rune;
buffs[i].magic_rune = b.magic_rune;
buffs[i].dot_rune = b.dot_rune;
buffs[i].caston_x = b.caston_x;
buffs[i].caston_y = b.caston_y;
buffs[i].caston_z = b.caston_z;
buffs[i].ExtraDIChance = b.ExtraDIChance;
buffs[i].RootBreakChance = b.RootBreakChance;
buffs[i].instrument_mod = b.instrument_mod;
buffs[i].virus_spread_time = b.virus_spread_time;
buffs[i].persistant_buff = b.persistant_buff;
buffs[i].client = b.client;
buffs[i].UpdateClient = b.UpdateClient;
i++;
}
CalcBonuses();
}
protected:
void HandleRoambox();
+28 -8
View File
@@ -58,7 +58,7 @@ void Client::SendBulkParcels()
p.second.aug_slot_6
));
if (inst) {
inst->SetCharges(p.second.quantity > 0 ? p.second.quantity : 1);
inst->SetCharges(p.second.quantity);
inst->SetMerchantCount(1);
inst->SetMerchantSlot(p.second.slot_id);
if (inst->IsStackable()) {
@@ -161,7 +161,7 @@ void Client::SendParcel(Parcel_Struct &parcel_in)
p.aug_slot_6
));
if (inst) {
inst->SetCharges(p.quantity > 0 ? p.quantity : 1);
inst->SetCharges(p.quantity);
inst->SetMerchantCount(1);
inst->SetMerchantSlot(p.slot_id);
if (inst->IsStackable()) {
@@ -272,6 +272,10 @@ void Client::SendParcelStatus()
void Client::DoParcelSend(const Parcel_Struct *parcel_in)
{
if (IsCasting()) {
StopCasting();
}
auto send_to_client = CharacterParcelsRepository::GetParcelCountAndCharacterName(database, parcel_in->send_to);
auto merchant = entity_list.GetMob(parcel_in->npc_id);
if (!merchant) {
@@ -382,7 +386,7 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in)
quantity = parcel_in->quantity;
}
else {
quantity = inst->GetCharges() > 0 ? inst->GetCharges() : parcel_in->quantity;
quantity = inst->GetCharges() >= 0 ? inst->GetCharges() : parcel_in->quantity;
}
CharacterParcelsRepository::CharacterParcels parcel_out{};
@@ -434,13 +438,13 @@ void Client::DoParcelSend(const Parcel_Struct *parcel_in)
cpc.aug_slot_5 = augs.at(4);
cpc.aug_slot_6 = augs.at(5);
}
cpc.quantity = kv.second->GetCharges() > 0 ? kv.second->GetCharges() : 1;
cpc.quantity = kv.second->GetCharges() >= 0 ? kv.second->GetCharges() : 1;
all_entries.push_back(cpc);
}
CharacterParcelsContainersRepository::InsertMany(database, all_entries);
}
RemoveItemBySerialNumber(inst->GetSerialNumber(), parcel_out.quantity);
RemoveItemBySerialNumber(inst->GetSerialNumber(), parcel_out.quantity == 0 ? 1 : parcel_out.quantity);
std::unique_ptr<EQApplicationPacket> outapp(new EQApplicationPacket(OP_ShopSendParcel));
QueuePacket(outapp.get());
@@ -642,9 +646,9 @@ void Client::DoParcelRetrieve(const ParcelRetrieve_Struct &parcel_in)
if (p != m_parcels.end()) {
uint32 item_id = parcel_in.parcel_item_id;
uint32 item_quantity = p->second.quantity;
if (!item_id || !item_quantity) {
if (!item_id) {
LogError(
"Attempt to retrieve parcel with erroneous item id or quantity for client character id {}.",
"Attempt to retrieve parcel with erroneous item id for client character id {}.",
CharacterID()
);
SendParcelRetrieveAck();
@@ -884,6 +888,22 @@ void Client::AddParcel(CharacterParcelsRepository::CharacterParcels &parcel)
"Unable to send parcel at this time. Please try again later."
);
SendParcelAck();
return;
}
}
int32 Client::FindNextFreeParcelSlotUsingMemory()
{
auto const results = GetParcels();
if (results.empty()) {
return PARCEL_BEGIN_SLOT;
}
for (uint32 i = PARCEL_BEGIN_SLOT; i <= RuleI(Parcel, ParcelMaxItems); i++) {
if (!results.contains(i)) {
return i;
}
}
return INVALID_INDEX;
}
+43
View File
@@ -561,6 +561,43 @@ std::string Perl_Zone_GetBucketRemaining(Zone* self, const std::string bucket_na
return self->GetBucketRemaining(bucket_name);
}
void Perl_Zone_ClearVariables(Zone* self)
{
self->ClearVariables();
}
bool Perl_Zone_DeleteVariable(Zone* self, const std::string variable_name)
{
return self->DeleteVariable(variable_name);
}
std::string Perl_Zone_GetVariable(Zone* self, const std::string variable_name)
{
return self->GetVariable(variable_name);
}
perl::array Perl_Zone_GetVariables(Zone* self)
{
perl::array a;
const auto& l = self->GetVariables();
for (const auto& v : l) {
a.push_back(v);
}
return a;
}
void Perl_Zone_SetVariable(Zone* self, const std::string variable_name, const std::string variable_value)
{
self->SetVariable(variable_name, variable_value);
}
bool Perl_Zone_VariableExists(Zone* self, const std::string variable_name)
{
return self->VariableExists(variable_name);
}
void perl_register_zone()
{
perl::interpreter perl(PERL_GET_THX);
@@ -573,7 +610,9 @@ void perl_register_zone()
package.add("CanDoCombat", &Perl_Zone_CanDoCombat);
package.add("CanLevitate", &Perl_Zone_CanLevitate);
package.add("ClearSpawnTimers", &Perl_Zone_ClearSpawnTimers);
package.add("ClearVariables", &Perl_Zone_ClearVariables);
package.add("DeleteBucket", &Perl_Zone_DeleteBucket);
package.add("DeleteVariable", &Perl_Zone_DeleteVariable);
package.add("Depop", (void(*)(Zone*))&Perl_Zone_Depop);
package.add("Depop", (void(*)(Zone*, bool))&Perl_Zone_Depop);
package.add("Despawn", &Perl_Zone_Despawn);
@@ -655,6 +694,8 @@ void perl_register_zone()
package.add("GetZoneType", &Perl_Zone_GetZoneType);
package.add("GetUnderworld", &Perl_Zone_GetUnderworld);
package.add("GetUnderworldTeleportIndex", &Perl_Zone_GetUnderworldTeleportIndex);
package.add("GetVariable", &Perl_Zone_GetVariable);
package.add("GetVariables", &Perl_Zone_GetVariables);
package.add("GetWalkSpeed", &Perl_Zone_GetWalkSpeed);
package.add("GetZoneZType", &Perl_Zone_GetZoneZType);
package.add("GetZoneTotalBlockedSpells", &Perl_Zone_GetZoneTotalBlockedSpells);
@@ -685,7 +726,9 @@ void perl_register_zone()
package.add("SetInstanceTimer", &Perl_Zone_SetInstanceTimer);
package.add("SetInstanceTimeRemaining", &Perl_Zone_SetInstanceTimeRemaining);
package.add("SetIsHotzone", &Perl_Zone_SetIsHotzone);
package.add("SetVariable", &Perl_Zone_SetVariable);
package.add("ShowZoneGlobalLoot", &Perl_Zone_ShowZoneGlobalLoot);
package.add("VariableExists", &Perl_Zone_VariableExists);
}
#endif //EMBPERL_XS_CLASSES
+8 -5
View File
@@ -165,11 +165,14 @@ void Mob::MakePoweredPet(uint16 spell_id, const char* pettype, int16 petpower,
// 4 - Keep DB name
// 5 - `s ward
if (IsClient() && !petname) {
const auto vanity_name = CharacterPetNameRepository::FindOne(database, CastToClient()->CharacterID());
if (!vanity_name.name.empty()) {
petname = vanity_name.name.c_str();
}
const auto vanity_name = (IsClient() && !petname) ? CharacterPetNameRepository::FindOne(database, CastToClient()->CharacterID()) : CharacterPetNameRepository::CharacterPetName{};
if (
IsClient() &&
!petname &&
!vanity_name.name.empty()
) {
petname = vanity_name.name.c_str();
}
if (petname != nullptr) {
+6
View File
@@ -435,6 +435,12 @@ int QuestParserCollection::EventNPC(
std::vector<std::any>* extra_pointers
)
{
if (npc->IsResumedFromZoneSuspend()) {
if (event_id == EVENT_DEATH_COMPLETE || event_id == EVENT_DEATH) {
return 0;
}
}
const int local_return = EventNPCLocal(event_id, npc, init, data, extra_data, extra_pointers);
const int global_return = EventNPCGlobal(event_id, npc, init, data, extra_data, extra_pointers);
const int default_return = DispatchEventNPC(event_id, npc, init, data, extra_data, extra_pointers);
+14 -1
View File
@@ -198,7 +198,13 @@ bool Spawn2::Process() {
}
//have the spawn group pick an NPC for us
uint32 npcid = currentnpcid && currentnpcid > 0 ? currentnpcid : spawn_group->GetNPCType(condition_value);
uint32 npcid = 0;
if (RuleB(Zone, StateSavingOnShutdown) && currentnpcid && currentnpcid > 0) {
npcid = currentnpcid;
} else {
npcid = spawn_group->GetNPCType(condition_value);
}
if (npcid == 0) {
LogSpawns("Spawn2 [{}]: Spawn group [{}] did not yeild an NPC! not spawning", spawn2_id, spawngroup_id_);
@@ -356,6 +362,7 @@ void Spawn2::LoadGrid(int start_wp) {
void Spawn2::Reset() {
timer.Start(resetTimer());
npcthis = nullptr;
currentnpcid = 0;
LogSpawns("Spawn2 [{}]: Spawn reset, repop in [{}] ms", spawn2_id, timer.GetRemainingTime());
}
@@ -363,6 +370,7 @@ void Spawn2::Depop() {
timer.Disable();
LogSpawns("Spawn2 [{}]: Spawn reset, repop disabled", spawn2_id);
npcthis = nullptr;
currentnpcid = 0;
}
void Spawn2::Repop(uint32 delay) {
@@ -374,6 +382,7 @@ void Spawn2::Repop(uint32 delay) {
timer.Start(delay);
}
npcthis = nullptr;
currentnpcid = 0;
}
void Spawn2::ForceDespawn()
@@ -392,12 +401,14 @@ void Spawn2::ForceDespawn()
npcthis->Depop(true);
IsDespawned = true;
npcthis = nullptr;
currentnpcid = 0;
return;
}
else
{
npcthis->Depop(false);
npcthis = nullptr;
currentnpcid = 0;
}
}
}
@@ -429,6 +440,7 @@ void Spawn2::DeathReset(bool realdeath)
//zero out our NPC since he is now gone
npcthis = nullptr;
currentnpcid = 0;
if(realdeath) { killcount++; }
@@ -643,6 +655,7 @@ void Spawn2::SpawnConditionChanged(const SpawnCondition &c, int16 old_value) {
LogSpawns("Spawn2 [{}]: Our npcthis is currently not null. The zone thinks it is [{}]. Forcing a depop", spawn2_id, npcthis->GetName());
npcthis->Depop(false); //remove the current mob
npcthis = nullptr;
currentnpcid = 0;
}
if(new_state) { // only get repawn timer remaining when the SpawnCondition is enabled.
timer_remaining = database.GetSpawnTimeLeft(spawn2_id,zone->GetInstanceID());
+1 -1
View File
@@ -1131,7 +1131,7 @@ bool Client::TradeskillExecute(DBTradeskillRecipe_Struct *spec) {
zone->random.Roll(aa_chance)
) {
if (GetGM()) {
Message(Chat::White, "Your GM flag gives you a 100% chance to succeed in combining this tradeskill.");
Message(Chat::White, "Your GM flag gives you a 100%% chance to succeed in combining this tradeskill.");
}
success_modifier = 1;
+3 -3
View File
@@ -1465,7 +1465,7 @@ void Client::BuyTraderItem(TraderBuy_Struct *tbs, Client *Trader, const EQApplic
Trader->AddMoneyToPP(copper, silver, gold, platinum, true);
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
if (buy_item && player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
auto e = PlayerEvent::TraderPurchaseEvent{
.item_id = buy_item->GetID(),
.augment_1_id = buy_item->GetAugmentItemID(0),
@@ -1487,7 +1487,7 @@ void Client::BuyTraderItem(TraderBuy_Struct *tbs, Client *Trader, const EQApplic
RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e);
}
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) {
if (buy_item && player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) {
auto e = PlayerEvent::TraderSellEvent{
.item_id = buy_item->GetID(),
.augment_1_id = buy_item->GetAugmentItemID(0),
@@ -2975,7 +2975,7 @@ void Client::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicati
Message(Chat::Red, fmt::format("You paid {} for the parcel delivery.", DetermineMoneyString(fee)).c_str());
LogTrading("Customer <green>[{}] Paid: <green>[{}] in Copper", CharacterID(), total_cost);
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
if (buy_item && player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
auto e = PlayerEvent::TraderPurchaseEvent{
.item_id = buy_item->GetID(),
.augment_1_id = buy_item->GetAugmentItemID(0),
+11 -6
View File
@@ -3410,6 +3410,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
case ServerOP_DzRemoveAllMembers:
case ServerOP_DzDurationUpdate:
case ServerOP_DzGetMemberStatuses:
case ServerOP_DzGetBulkMemberStatuses:
case ServerOP_DzSetCompass:
case ServerOP_DzSetSafeReturn:
case ServerOP_DzSetZoneIn:
@@ -3799,7 +3800,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
auto item = trader_pc->FindTraderItemBySerialNumber(item_sn);
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) {
if (item && player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) {
auto e = PlayerEvent::TraderSellEvent{
.item_id = item ? item->GetID() : 0,
.augment_1_id = item->GetAugmentItemID(0),
@@ -4661,16 +4662,20 @@ void WorldServer::ProcessReload(const ServerReload::Request& request)
break;
case ServerReload::Type::WorldRepop:
entity_list.ClearAreas();
parse->ReloadQuests();
zone->Repop();
if (zone && zone->IsLoaded()) {
entity_list.ClearAreas();
zone->Repop();
}
break;
case ServerReload::Type::WorldWithRespawn:
entity_list.ClearAreas();
parse->ReloadQuests();
zone->Repop();
zone->ClearSpawnTimers();
if (zone && zone->IsLoaded()) {
entity_list.ClearAreas();
zone->Repop();
zone->ClearSpawnTimers();
}
break;
case ServerReload::Type::ZonePoints:
+67
View File
@@ -3218,5 +3218,72 @@ void Zone::DisableRespawnTimers()
}
}
void Zone::ClearVariables()
{
m_zone_variables.clear();
}
bool Zone::DeleteVariable(const std::string& variable_name)
{
if (m_zone_variables.empty() || variable_name.empty()) {
return false;
}
auto v = m_zone_variables.find(variable_name);
if (v == m_zone_variables.end()) {
return false;
}
m_zone_variables.erase(v);
return true;
}
std::string Zone::GetVariable(const std::string& variable_name)
{
if (m_zone_variables.empty() || variable_name.empty()) {
return std::string();
}
const auto& v = m_zone_variables.find(variable_name);
return v != m_zone_variables.end() ? v->second : std::string();
}
std::vector<std::string> Zone::GetVariables()
{
std::vector<std::string> l;
if (m_zone_variables.empty()) {
return l;
}
l.reserve(m_zone_variables.size());
for (const auto& v : m_zone_variables) {
l.emplace_back(v.first);
}
return l;
}
void Zone::SetVariable(const std::string& variable_name, const std::string& variable_value)
{
if (variable_name.empty()) {
return;
}
m_zone_variables[variable_name] = variable_value;
}
bool Zone::VariableExists(const std::string& variable_name)
{
if (m_zone_variables.empty() || variable_name.empty()) {
return false;
}
return m_zone_variables.find(variable_name) != m_zone_variables.end();
}
#include "zone_save_state.cpp"
#include "zone_loot.cpp"
+9
View File
@@ -197,6 +197,13 @@ public:
int32 MobsAggroCount() { return aggroedmobs; }
DynamicZone *GetDynamicZone();
void ClearVariables();
bool DeleteVariable(const std::string& variable_name);
std::string GetVariable(const std::string& variable_name);
std::vector<std::string> GetVariables();
void SetVariable(const std::string& variable_name, const std::string& variable_value);
bool VariableExists(const std::string& variable_name);
IPathfinder *pathing;
std::vector<NPC_Emote_Struct *> npc_emote_list;
LinkedList<Spawn2 *> spawn2_list;
@@ -244,6 +251,8 @@ public:
std::vector<uint32> discovered_items;
std::map<std::string, std::string> m_zone_variables;
time_t weather_timer;
Timer spawn2_timer;
Timer hot_reload_timer;
+2
View File
@@ -31,12 +31,14 @@ void ZoneCLI::CommandHandler(int argc, char **argv)
// Register commands
function_map["benchmark:databuckets"] = &ZoneCLI::BenchmarkDatabuckets;
function_map["sidecar:serve-http"] = &ZoneCLI::SidecarServeHttp;
function_map["tests:databuckets"] = &ZoneCLI::DataBuckets;
function_map["tests:npc-handins"] = &ZoneCLI::NpcHandins;
function_map["tests:npc-handins-multiquest"] = &ZoneCLI::NpcHandinsMultiQuest;
EQEmuCommand::HandleMenu(function_map, cmd, argc, argv);
}
#include "cli/databuckets.cpp"
#include "cli/benchmark_databuckets.cpp"
#include "cli/sidecar_serve_http.cpp"
#include "cli/npc_handins.cpp"
+1
View File
@@ -11,6 +11,7 @@ public:
static bool RanConsoleCommand(int argc, char **argv);
static bool RanSidecarCommand(int argc, char **argv);
static bool RanTestCommand(int argc, char **argv);
static void DataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description);
static void NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description);
static void NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std::string &description);
};
+101 -12
View File
@@ -49,6 +49,11 @@ inline void LoadLootStateData(Zone *zone, NPC *npc, const std::string &loot_data
{
LootStateData l{};
if (!Strings::IsValidJson(loot_data)) {
LogZoneState("Invalid JSON data for NPC [{}]", npc->GetNPCTypeID());
return;
}
try {
std::stringstream ss;
{
@@ -166,6 +171,15 @@ inline std::string GetLootSerialized(Corpse *c)
inline void LoadNPCEntityVariables(NPC *n, const std::string &entity_variables)
{
if (!RuleB(Zone, StateSaveEntityVariables)) {
return;
}
if (!Strings::IsValidJson(entity_variables)) {
LogZoneState("Invalid JSON data for NPC [{}]", n->GetNPCTypeID());
return;
}
std::map<std::string, std::string> deserialized_map;
try {
std::istringstream is(entity_variables);
@@ -186,6 +200,15 @@ inline void LoadNPCEntityVariables(NPC *n, const std::string &entity_variables)
inline void LoadNPCBuffs(NPC *n, const std::string &buffs)
{
if (!RuleB(Zone, StateSaveBuffs)) {
return;
}
if (!Strings::IsValidJson(buffs)) {
LogZoneState("Invalid JSON data for NPC [{}]", n->GetNPCTypeID());
return;
}
std::vector<Buffs_Struct> valid_buffs;
try {
std::istringstream is(buffs);
@@ -199,10 +222,7 @@ inline void LoadNPCBuffs(NPC *n, const std::string &buffs)
return;
}
for (const auto &b: valid_buffs) {
// int AddBuff(Mob *caster, const uint16 spell_id, int duration = 0, int32 level_override = -1, bool disable_buff_overwrite = false);
n->AddBuff(n, b.spellid, b.ticsremaining, b.casterlevel, false);
}
n->LoadBuffsFromState(valid_buffs);
}
inline std::vector<uint32_t> GetLootdropIds(const std::vector<ZoneStateSpawnsRepository::ZoneStateSpawns> &spawn_states)
@@ -210,7 +230,8 @@ inline std::vector<uint32_t> GetLootdropIds(const std::vector<ZoneStateSpawnsRep
LogInfo("Loading lootdrop ids for zone state spawns");
std::vector<uint32_t> lootdrop_ids;
for (auto &s: spawn_states) {
for (auto &s: spawn_states) {
if (s.loot_data.empty()) {
continue;
}
@@ -270,6 +291,55 @@ inline void LoadNPCState(Zone *zone, NPC *n, ZoneStateSpawnsRepository::ZoneStat
n->SetResumedFromZoneSuspend(true);
}
inline std::string GetZoneVariablesSerialized(Zone *z)
{
std::map<std::string, std::string> variables;
for (const auto &k: z->GetVariables()) {
variables[k] = z->GetVariable(k);
}
try {
std::ostringstream os;
{
cereal::JSONOutputArchiveSingleLine archive(os);
archive(variables);
}
return os.str();
}
catch (const std::exception &e) {
LogZoneState("Failed to serialize variables for zone [{}]", e.what());
return "";
}
return "";
}
inline void LoadZoneVariables(Zone *z, const std::string &variables)
{
if (!Strings::IsValidJson(variables)) {
LogZoneState("Invalid JSON data for zone [{}]", variables);
return;
}
std::map<std::string, std::string> deserialized_map;
try {
std::istringstream is(variables);
{
cereal::JSONInputArchive archive(is);
archive(deserialized_map);
}
}
catch (const std::exception &e) {
LogZoneState("Failed to load zone variables [{}]", e.what());
return;
}
for (const auto &[key, value]: deserialized_map) {
z->SetVariable(key, value);
}
}
bool Zone::LoadZoneState(
std::unordered_map<uint32, uint32> spawn_times,
std::vector<Spawn2DisabledRepository::Spawn2Disabled> disabled_spawns
@@ -294,11 +364,12 @@ bool Zone::LoadZoneState(
zone->Process();
for (auto &s: spawn_states) {
if (s.spawngroup_id == 0) {
if (s.is_zone) {
LoadZoneVariables(zone, s.entity_variables);
continue;
}
if (s.is_corpse) {
if (s.spawngroup_id == 0 || s.is_corpse || s.is_zone) {
continue;
}
@@ -332,7 +403,7 @@ bool Zone::LoadZoneState(
(bool) s.path_when_zone_idle,
s.condition_id,
s.condition_min_value,
spawn_enabled,
(s.enabled && spawn_enabled),
(EmuAppearance) s.anim
);
@@ -353,7 +424,7 @@ bool Zone::LoadZoneState(
// dynamic spawns, quest spawns, triggers etc.
for (auto &s: spawn_states) {
if (s.spawngroup_id > 0) {
if (s.spawngroup_id > 0 || s.is_zone) {
continue;
}
@@ -400,7 +471,8 @@ inline void SaveNPCState(NPC *n, ZoneStateSpawnsRepository::ZoneStateSpawns &s)
{
// entity variables
std::map<std::string, std::string> variables;
for (const auto &k: n->GetEntityVariables()) {
for (const auto &k: n->GetEntityVariables()) {
variables[k] = n->GetEntityVariable(k);
}
@@ -488,7 +560,7 @@ void Zone::SaveZoneState()
s.created_at = std::time(nullptr);
auto n = sp->GetNPC();
if (n) {
if (n && entity_list.GetNPCByID(n->GetID())) {
SaveNPCState(n, s);
}
@@ -502,7 +574,13 @@ void Zone::SaveZoneState()
bool ignore_npcs =
n.second->GetSpawnGroupId() > 0 ||
n.second->GetNPCTypeID() < 100 ||
n.second->HasOwner();
n.second->GetNPCTypeID() == 500 || // Trap::CreateHiddenTrigger
n.second->IsAura() ||
n.second->IsBot() ||
n.second->IsMerc() ||
n.second->IsTrap() ||
n.second->GetSwarmOwner() ||
n.second->IsPet();
if (ignore_npcs) {
continue;
}
@@ -538,6 +616,17 @@ void Zone::SaveZoneState()
spawns.emplace_back(s);
}
// zone state variables
if (!GetVariables().empty()) {
ZoneStateSpawnsRepository::ZoneStateSpawns z{};
z.zone_id = GetZoneID();
z.instance_id = GetInstanceID();
z.is_zone = 1;
z.entity_variables = GetZoneVariablesSerialized(this);
spawns.emplace_back(z);
}
ZoneStateSpawnsRepository::DeleteWhere(
database,
fmt::format(