mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-24 18:22:29 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8203c034bf | |||
| 33ae51f56f | |||
| 30c39194a3 | |||
| 051ce3736f | |||
| 84708edccf | |||
| da4e9ab95b | |||
| a8fea95eab | |||
| 53610c2f0f | |||
| 9f10c12874 | |||
| a0634adb3c | |||
| a2ed6be1f5 | |||
| c33ac40567 | |||
| 9ee095b354 | |||
| 7ab32af4dc | |||
| 0c301419c2 | |||
| d6a21be25e | |||
| 1d4ba082ad | |||
| 94553501ba | |||
| da824d5178 | |||
| 5a1df38900 | |||
| 8cd7148b29 | |||
| 09e079a45e | |||
| 4bc881da4b | |||
| 0615864d51 | |||
| 3638d157b2 | |||
| d41725e325 | |||
| 88580b69b6 | |||
| f7a6fe595a | |||
| 07d14c2681 | |||
| eac7a73fb6 | |||
| c5715f1f14 | |||
| 3902230fa1 | |||
| 212969f5cd | |||
| de4226fdc9 | |||
| 8b13434197 | |||
| 27274397ec |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ public:
|
||||
*/
|
||||
void CloseFileLogs();
|
||||
EQEmuLogSys *LoadLogSettingsDefaults();
|
||||
EQEmuLogSys *LoadLogDatabaseSettings();
|
||||
EQEmuLogSys *LoadLogDatabaseSettings(bool silent_load = false);
|
||||
|
||||
/**
|
||||
* @param directory_name
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user