Compare commits

...

10 Commits

Author SHA1 Message Date
Chris Miles 588de15382 ? 2025-06-29 23:39:33 -05:00
Chris Miles aeb13e8fd3 ? 2025-06-29 23:38:45 -05:00
Chris Miles 98f824b0f5 Build tweaks 2025-06-29 23:34:46 -05:00
Chris Miles 605e23e8d0 Build analyze work 2025-06-29 22:29:36 -05:00
nytmyr fc470d5f83 [Bots] Fix ^cast resurrects (#4958)
- Was failing stack checks and couldn't be casted. Added bypass.
2025-06-29 10:19:00 -04:00
Chris Miles 585ed3bd25 [Release] 23.8.1 2025-06-28 17:29:25 -05:00
Chris Miles 2eb291a461 [Databuckets] Revert Caching Changes of #4917 (#4957)
* [Databuckets] Revert Caching Changes of #4917

* Comment caching tests until addressed later
2025-06-28 17:28:19 -05:00
Alex King a1421af214 [Bug Fix] Fix Hero Forge on Character Select (#4954) 2025-06-26 19:35:28 -04:00
Alex King 13aad6229f [Crash Fix] Fix Possible Crashes with Raid Methods (#4955) 2025-06-26 19:35:20 -04:00
Alex King 76d46ceaf0 [Bug Fix] Fix FindCharacter Using content_db (#4956) 2025-06-26 19:35:11 -04:00
17 changed files with 209 additions and 198 deletions
+33 -1
View File
@@ -30,6 +30,11 @@ steps:
- name: cache
path: /home/eqemu/.ccache/
trigger:
branch:
exclude:
- build-analyze
---
kind: pipeline
@@ -57,6 +62,11 @@ steps:
commands:
- .\utils\scripts\build\windows-build.ps1
trigger:
branch:
exclude:
- build-analyze
---
kind: pipeline
@@ -87,7 +97,10 @@ steps:
trigger:
branch:
- master
include:
- master
exclude:
- build-analyze
event:
- push
@@ -95,4 +108,23 @@ depends_on:
- Build Windows
- Build Linux
---
kind: pipeline
type: docker
name: Build Analyze
steps:
- name: Build Linux X64
image: akkadius/eqemu-server:v16
environment:
GITHUB_TOKEN:
from_secret: GH_RELEASE_GITHUB_API_TOKEN
commands:
- ./utils/scripts/build/linux-build-analyze.sh
trigger:
branch:
- build-analyze
event:
- push
+15
View File
@@ -1,3 +1,18 @@
## [23.8.1] 6/28/2025
### Crash Fix
* Fix Possible Crashes with Raid Methods ([#4955](https://github.com/EQEmu/Server/pull/4955)) @Kinglykrab 2025-06-26
### Databuckets
* Revert Caching Changes of #4917 ([#4957](https://github.com/EQEmu/Server/pull/4957)) @Akkadius 2025-06-28
### Fixes
* Fix FindCharacter Using content_db ([#4956](https://github.com/EQEmu/Server/pull/4956)) @Kinglykrab 2025-06-26
* Fix Hero Forge on Character Select ([#4954](https://github.com/EQEmu/Server/pull/4954)) @Kinglykrab 2025-06-26
## [23.8.0] 6/25/2025
### API
+2
View File
@@ -363,6 +363,8 @@ MESSAGE(STATUS "**************************************************")
#setup server libs and headers
SET(SERVER_LIBS common ${DATABASE_LIBRARY_LIBS} ${ZLIB_LIBRARY_LIBS} ${Boost_LIBRARIES} uv_a fmt RecastNavigation::Detour)
set(FMT_HEADER_ONLY OFF)
INCLUDE_DIRECTORIES(SYSTEM "${DATABASE_LIBRARY_INCLUDE}")
INCLUDE_DIRECTORIES(SYSTEM "${ZLIB_LIBRARY_INCLUDE}")
INCLUDE_DIRECTORIES(SYSTEM "${Boost_INCLUDE_DIRS}")
+1 -1
View File
@@ -841,7 +841,7 @@ IF (UNIX)
SET_SOURCE_FILES_PROPERTIES("patches/sod.cpp" "patches/sof.cpp" "patches/rof.cpp" "patches/rof2.cpp" "patches/uf.cpp" PROPERTIES COMPILE_FLAGS -O0)
ENDIF (UNIX)
IF (WIN32 AND EQEMU_BUILD_PCH)
IF (EQEMU_BUILD_PCH)
TARGET_PRECOMPILE_HEADERS(common PRIVATE pch/std-pch.h)
ENDIF ()
+8 -82
View File
@@ -19,37 +19,6 @@ extern WorldDatabase database;
#error "You must define either ZONE or WORLD"
#endif
// Key: compound cache key (e.g., account_id|character_id|zone_id|instance_id|top_key|full_key)
// Value: resolved DataBuckets with extracted nested value
static std::unordered_map<std::string, DataBucketsRepository::DataBuckets> g_nested_bucket_cache;
static std::string MakeNestedCacheKey(const DataBucketKey &k, const std::string &full_key) {
return fmt::format(
"account_id:{}|character_id:{}|npc_id:{}|bot_id:{}|zone_id:{}|instance_id:{}|top_key:{}|full_key:{}",
k.account_id, k.character_id, k.npc_id, k.bot_id, k.zone_id, k.instance_id,
Strings::Split(full_key, NESTED_KEY_DELIMITER).front(),
full_key
);
}
static std::string MakeNestedCacheKeyPrefix(const DataBucketKey &k, const std::string &top_key) {
return fmt::format(
"account_id:{}|character_id:{}|npc_id:{}|bot_id:{}|zone_id:{}|instance_id:{}|top_key:{}|",
k.account_id, k.character_id, k.npc_id, k.bot_id, k.zone_id, k.instance_id, top_key
);
}
static void InvalidateNestedCacheForKey(const DataBucketKey &k, const std::string &top_key) {
std::string prefix = MakeNestedCacheKeyPrefix(k, top_key);
for (auto it = g_nested_bucket_cache.begin(); it != g_nested_bucket_cache.end(); ) {
if (it->first.find(prefix) == 0) {
it = g_nested_bucket_cache.erase(it);
} else {
++it;
}
}
}
void DataBucket::SetData(const std::string &bucket_key, const std::string &bucket_value, std::string expires_time)
{
auto k = DataBucketKey{
@@ -167,15 +136,6 @@ void DataBucket::SetData(const DataBucketKey &k_)
// Serialize JSON back to string
b.value = json_value.dump();
b.key_ = top_key; // Use the top-level key
if (CanCache(k_)) {
InvalidateNestedCacheForKey(k_, top_key);
std::string nested_cache_key = MakeNestedCacheKey(k_, k_.key);
auto extracted = ExtractNestedValue(b, k_.key);
if (extracted.id > 0) {
g_nested_bucket_cache[nested_cache_key] = extracted;
}
}
}
if (bucket_id) {
@@ -291,27 +251,12 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k_,
LogDataBuckets("Returning key [{}] value [{}] from cache", e.key_, e.value);
if (is_nested_key && !k_.key.empty()) {
std::string nested_cache_key = MakeNestedCacheKey(k_, k.key);
auto it = g_nested_bucket_cache.find(nested_cache_key);
if (it != g_nested_bucket_cache.end()) {
LogDataBucketsDetail("Nested cache hit for key [{}]", nested_cache_key);
return it->second;
}
auto extracted = ExtractNestedValue(e, k_.key);
if (extracted.id > 0) {
g_nested_bucket_cache[nested_cache_key] = extracted;
}
return extracted;
return ExtractNestedValue(e, k_.key);
}
return e;
}
}
// if we can cache its assumed we didn't load this into the cache so we should not return a miss
return DataBucketsRepository::NewEntity(); // Not found in cache
}
// Fetch the value from the database
@@ -370,42 +315,23 @@ DataBucketsRepository::DataBuckets DataBucket::GetData(const DataBucketKey &k_,
}
// Add the value to the cache if it doesn't exist
// If cacheable and not found in cache, short-circuit and assume it doesn't exist
if (can_cache) {
bool found_in_cache = false;
bool has_cache = false;
for (const auto &e : g_data_bucket_cache) {
if (CheckBucketMatch(e, k)) {
found_in_cache = true;
if (e.id == bucket.id) {
has_cache = true;
break;
}
}
if (!found_in_cache) {
LogDataBuckets("Cache miss for key [{}] - skipping DB due to CanCache", k.key);
return DataBucketsRepository::NewEntity();
if (!has_cache) {
g_data_bucket_cache.emplace_back(bucket);
}
}
// Handle nested key extraction
if (is_nested_key && !k_.key.empty()) {
if (CanCache(k_)) {
std::string nested_cache_key = MakeNestedCacheKey(k_, k.key);
auto it = g_nested_bucket_cache.find(nested_cache_key);
if (it != g_nested_bucket_cache.end()) {
LogDataBucketsDetail("Nested cache hit for key [{}]", nested_cache_key);
return it->second;
}
auto extracted = ExtractNestedValue(bucket, k_.key);
if (extracted.id > 0) {
g_nested_bucket_cache[nested_cache_key] = extracted;
}
return extracted;
} else {
// Not cacheable, just extract and return
return ExtractNestedValue(bucket, k_.key);
}
return ExtractNestedValue(bucket, k_.key);
}
return bucket;
@@ -917,4 +843,4 @@ bool DataBucket::CanCache(const DataBucketKey &key)
}
return false;
}
}
+1 -1
View File
@@ -574,7 +574,7 @@ EQ::ItemInstance* EQ::ItemInstance::GetOrnamentationAugment() const
uint32 EQ::ItemInstance::GetOrnamentHeroModel(int32 material_slot) const
{
// Not a Hero Forge item.
if (m_ornament_hero_model == 0 || material_slot < 0) {
if (m_ornament_hero_model == 0) {
return 0;
}
+7 -27
View File
@@ -1,34 +1,14 @@
// types
#include <limits>
#include <string>
#include <cctype>
#include <sstream>
#pragma once
// containers
#include <iterator>
#include <set>
#include <unordered_set>
// Lightweight, widely used
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <list>
#include <vector>
// utilities
#include <iostream>
#include <cassert>
#include <cmath>
#include <memory>
#include <functional>
#include <algorithm>
#include <utility>
#include <tuple>
#include <fstream>
#include <cstdio>
#include <limits>
#include <cstdint>
#include <cassert>
// fmt
#include <fmt/format.h>
// lua
#include "lua.hpp"
#include <luabind/luabind.hpp>
#include <luabind/object.hpp>
+1
View File
@@ -358,6 +358,7 @@ bool RequiresStackCheck(uint16 spell_type) {
case BotSpellTypes::CompleteHeal:
case BotSpellTypes::PetCompleteHeals:
case BotSpellTypes::GroupCompleteHeals:
case BotSpellTypes::Resurrect:
return false;
default:
return true;
+1 -1
View File
@@ -25,7 +25,7 @@
// Build variables
// these get injected during the build pipeline
#define CURRENT_VERSION "23.8.0-dev" // always append -dev to the current version for custom-builds
#define CURRENT_VERSION "23.8.1-dev" // always append -dev to the current version for custom-builds
#define LOGIN_VERSION "0.8.0"
#define COMPILE_DATE __DATE__
#define COMPILE_TIME __TIME__
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eqemu-server",
"version": "23.8.0",
"version": "23.8.1",
"repository": {
"type": "git",
"url": "https://github.com/EQEmu/Server.git"
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
set -ex
sudo chown eqemu:eqemu /drone/src/ * -R
# Install ClangBuildAnalyzer if missing
if ! command -v clang-build-analyzer &> /dev/null; then
echo "Installing latest Clang Build Analyzer..."
LATEST_VERSION=$(curl -s https://api.github.com/repos/aras-p/ClangBuildAnalyzer/releases/latest \
| grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
sudo curl -sSL "https://github.com/aras-p/ClangBuildAnalyzer/releases/download/v${LATEST_VERSION}/ClangBuildAnalyzer-linux" \
-o /usr/local/bin/clang-build-analyzer
sudo chmod +x /usr/local/bin/clang-build-analyzer
fi
git submodule init && git submodule update
perl utils/scripts/build/tag-version.pl
mkdir -p build
clang-build-analyzer --start build/
cd build && \
cmake -DEQEMU_BUILD_TESTS=ON \
-DCMAKE_BUILD_TYPE=Debug \
-DEQEMU_BUILD_LUA=ON \
-DEQEMU_BUILD_PERL=ON \
-DEQEMU_BUILD_LOGIN=ON \
-DEQEMU_BUILD_STATIC=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_CXX_FLAGS:STRING="-O0 -g -Wno-everything -ftime-trace" \
-G 'Unix Makefiles' \
.. && \
make -j"$(nproc)"
# 🧠 Generate ClangBuildAnalyzer report
clang-build-analyzer --stop ./ /tmp/eqemu.capture
clang-build-analyzer --analyze /tmp/eqemu.capture > report.txt
cat report.txt
ldd ./bin/zone
cd /drone/src/
+1 -1
View File
@@ -76,7 +76,7 @@ ADD_EXECUTABLE(world ${world_sources} ${world_headers})
INSTALL(TARGETS world RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
IF (WIN32 AND EQEMU_BUILD_PCH)
IF (EQEMU_BUILD_PCH)
TARGET_PRECOMPILE_HEADERS(world PRIVATE ../common/pch/std-pch.h)
ENDIF ()
+1
View File
@@ -483,6 +483,7 @@ INSTALL(TARGETS zone RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
# precompiled headers
IF (EQEMU_BUILD_PCH)
TARGET_PRECOMPILE_HEADERS(zone PRIVATE ../common/pch/app-pch.h)
TARGET_PRECOMPILE_HEADERS(zone PRIVATE ../common/pch/std-pch.h)
TARGET_PRECOMPILE_HEADERS(zone PRIVATE ./pch/pch.h)
ENDIF()
+26 -26
View File
@@ -318,32 +318,32 @@ void ZoneCLI::TestDataBuckets(int argc, char** argv, argh::parser& cmd, std::str
);
// Cold cache test — should return ""
std::string cold_value = client->GetBucket(scoped_key);
RunTest("Cold Cache Scoped Key Returns Empty (Due to Skip DB)", "", cold_value);
// ✅ Reload cache
client->LoadDataBucketsCache();
// Cache should now return the value
std::string hot_value = client->GetBucket(scoped_key);
RunTest("Post-BulkLoad Scoped Key Returns Value", "cached_value", hot_value);
// Also test nested key after preload
client->DeleteBucket("ac_nested.test");
client->SetBucket("ac_nested.test", "nested_val");
// Clear cache, then preload
DataBucket::ClearCache();
client->LoadDataBucketsCache();
std::string nested_value = client->GetBucket("ac_nested.test");
RunTest("Post-BulkLoad Nested Scoped Key Returns Value", "nested_val", nested_value);
// Remove and check that cache misses properly again
client->DeleteBucket("ac_nested.test");
DataBucket::ClearCache();
std::string post_delete_check = client->GetBucket("ac_nested.test");
RunTest("Post-Delete Nested Scoped Key Returns Empty", "", post_delete_check);
// std::string cold_value = client->GetBucket(scoped_key);
// RunTest("Cold Cache Scoped Key Returns Empty (Due to Skip DB)", "", cold_value);
//
// // ✅ Reload cache
// client->LoadDataBucketsCache();
//
// // Cache should now return the value
// std::string hot_value = client->GetBucket(scoped_key);
// RunTest("Post-BulkLoad Scoped Key Returns Value", "cached_value", hot_value);
//
// // Also test nested key after preload
// client->DeleteBucket("ac_nested.test");
// client->SetBucket("ac_nested.test", "nested_val");
//
// // Clear cache, then preload
// DataBucket::ClearCache();
// client->LoadDataBucketsCache();
//
// std::string nested_value = client->GetBucket("ac_nested.test");
// RunTest("Post-BulkLoad Nested Scoped Key Returns Value", "nested_val", nested_value);
//
// // Remove and check that cache misses properly again
// client->DeleteBucket("ac_nested.test");
// DataBucket::ClearCache();
// std::string post_delete_check = client->GetBucket("ac_nested.test");
// RunTest("Post-Delete Nested Scoped Key Returns Empty", "", post_delete_check);
std::cout << "\n===========================================\n";
+5 -5
View File
@@ -4,9 +4,9 @@
void FindCharacter(Client *c, const Seperator *sep)
{
if (sep->IsNumber(2)) {
const auto character_id = Strings::ToUnsignedInt(sep->arg[2]);
const uint32 character_id = Strings::ToUnsignedInt(sep->arg[2]);
const auto& e = CharacterDataRepository::FindOne(content_db, character_id);
const auto& e = CharacterDataRepository::FindOne(database, character_id);
if (!e.id) {
c->Message(
Chat::White,
@@ -31,10 +31,10 @@ void FindCharacter(Client *c, const Seperator *sep)
return;
}
const auto search_criteria = Strings::ToLower(sep->argplus[2]);
const std::string& search_criteria = Strings::ToLower(sep->argplus[2]);
const auto& l = CharacterDataRepository::GetWhere(
content_db,
database,
fmt::format(
"LOWER(`name`) LIKE '%%{}%%' AND `name` NOT LIKE '%-deleted-%' ORDER BY `id` ASC LIMIT 50",
search_criteria
@@ -51,7 +51,7 @@ void FindCharacter(Client *c, const Seperator *sep)
);
}
auto found_count = 0;
uint32 found_count = 0;
for (const auto& e : l) {
c->Message(
+59 -49
View File
@@ -717,7 +717,7 @@ uint32 Raid::GetTotalRaidDamage(Mob* other)
return total;
}
void Raid::HealGroup(uint32 heal_amt, Mob* caster, uint32 gid, float range)
void Raid::HealGroup(uint32 heal_amount, Mob* caster, uint32 group_id, float range)
{
if (!caster) {
return;
@@ -728,26 +728,30 @@ void Raid::HealGroup(uint32 heal_amt, Mob* caster, uint32 gid, float range)
}
float distance;
float range2 = range*range;
float range_squared = range * range;
int member_count = 0;
int numMem = 0;
for (const auto& m : members) {
if (m.member && m.group_number == gid) {
if (m.member && m.group_number == group_id) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
if (distance <= range2) {
numMem += 1;
if (distance <= range_squared) {
member_count += 1;
}
}
}
heal_amt /= numMem;
if (member_count > 0) {
heal_amount /= member_count;
}
for (const auto& m : members) {
if (m.member && m.group_number == gid) {
if (m.member && m.group_number == group_id) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
if (distance <= range2) {
m.member->SetHP(m.member->GetHP() + heal_amt);
if (distance <= range_squared) {
m.member->SetHP(m.member->GetHP() + heal_amount);
m.member->SendHPUpdate();
}
}
@@ -755,7 +759,7 @@ void Raid::HealGroup(uint32 heal_amt, Mob* caster, uint32 gid, float range)
}
void Raid::BalanceHP(int32 penalty, uint32 gid, float range, Mob* caster, int32 limit)
void Raid::BalanceHP(int32 penalty, uint32 group_id, float range, Mob* caster, int32 limit)
{
if (!caster) {
return;
@@ -765,44 +769,48 @@ void Raid::BalanceHP(int32 penalty, uint32 gid, float range, Mob* caster, int32
range = 200;
}
int dmgtaken = 0, numMem = 0, dmgtaken_tmp = 0;
int damage_taken = 0;
int damage_taken_temporary = 0;
int member_count = 0;
float distance;
float range2 = range*range;
float range_squared = range * range;
for (const auto& m : members) {
if (m.member && m.group_number == gid) {
if (m.member && m.group_number == group_id) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
if (distance <= range2) {
dmgtaken_tmp = m.member->GetMaxHP() - m.member->GetHP();
if (distance <= range_squared) {
damage_taken_temporary = m.member->GetMaxHP() - m.member->GetHP();
if (limit && (dmgtaken_tmp > limit)) {
dmgtaken_tmp = limit;
if (limit && (damage_taken_temporary > limit)) {
damage_taken_temporary = limit;
}
dmgtaken += dmgtaken_tmp;
numMem += 1;
damage_taken += damage_taken_temporary;
member_count += 1;
}
}
}
dmgtaken += dmgtaken * penalty / 100;
dmgtaken /= numMem;
damage_taken += damage_taken * penalty / 100;
if (member_count > 0) {
damage_taken /= member_count;
}
for (const auto& m : members) {
if (m.member && m.group_number == gid) {
if (m.member && m.group_number == group_id) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
//this way the ability will never kill someone
//but it will come darn close
if (distance <= range2) {
if ((m.member->GetMaxHP() - dmgtaken) < 1) {
if (distance <= range_squared) {
if ((m.member->GetMaxHP() - damage_taken) < 1) {
m.member->SetHP(1);
m.member->SendHPUpdate();
}
else {
m.member->SetHP(m.member->GetMaxHP() - dmgtaken);
} else {
m.member->SetHP(m.member->GetMaxHP() - damage_taken);
m.member->SendHPUpdate();
}
}
@@ -810,7 +818,7 @@ void Raid::BalanceHP(int32 penalty, uint32 gid, float range, Mob* caster, int32
}
}
void Raid::BalanceMana(int32 penalty, uint32 gid, float range, Mob* caster, int32 limit)
void Raid::BalanceMana(int32 penalty, uint32 group_id, float range, Mob* caster, int32 limit)
{
if (!caster) {
return;
@@ -821,54 +829,56 @@ void Raid::BalanceMana(int32 penalty, uint32 gid, float range, Mob* caster, int3
}
float distance;
float range2 = range*range;
float range_squared = range * range;
int manataken = 0;
int numMem = 0;
int manataken_tmp = 0;
int mana_taken = 0;
int mana_taken_temporary = 0;
int member_count = 0;
for (const auto& m : members) {
if (m.is_bot) {
continue;
}
if (m.member && m.group_number == gid && m.member->GetMaxMana() > 0) {
if (m.member && m.group_number == group_id && m.member->GetMaxMana() > 0) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
if (distance <= range2) {
manataken_tmp = m.member->GetMaxMana() - m.member->GetMana();
if (distance <= range_squared) {
mana_taken_temporary = m.member->GetMaxMana() - m.member->GetMana();
if (limit && (manataken_tmp > limit)) {
manataken_tmp = limit;
if (limit && (mana_taken_temporary > limit)) {
mana_taken_temporary = limit;
}
manataken += manataken_tmp;
numMem += 1;
mana_taken += mana_taken_temporary;
member_count += 1;
}
}
}
manataken += manataken * penalty / 100;
manataken /= numMem;
mana_taken += mana_taken * penalty / 100;
if (member_count > 0) {
mana_taken /= member_count;
}
for (const auto& m : members) {
if (m.is_bot) {
continue;
}
if (m.member && m.group_number == gid) {
if (m.member && m.group_number == group_id) {
distance = DistanceSquared(caster->GetPosition(), m.member->GetPosition());
if (distance <= range2) {
if ((m.member->GetMaxMana() - manataken) < 1) {
if (distance <= range_squared) {
if ((m.member->GetMaxMana() - mana_taken) < 1) {
m.member->SetMana(1);
if (m.member->IsClient()) {
m.member->CastToClient()->SendManaUpdate();
}
}
else {
m.member->SetMana(m.member->GetMaxMana() - manataken);
} else {
m.member->SetMana(m.member->GetMaxMana() - mana_taken);
if (m.member->IsClient()) {
m.member->CastToClient()->SendManaUpdate();
+3 -3
View File
@@ -167,9 +167,9 @@ public:
void CastGroupSpell(Mob* caster,uint16 spellid, uint32 gid);
void SplitExp(ExpSource exp_source, const uint64 exp, Mob* other);
uint32 GetTotalRaidDamage(Mob* other);
void BalanceHP(int32 penalty, uint32 gid, float range = 0, Mob* caster = nullptr, int32 limit = 0);
void BalanceMana(int32 penalty, uint32 gid, float range = 0, Mob* caster = nullptr, int32 limit = 0);
void HealGroup(uint32 heal_amt, Mob* caster, uint32 gid, float range = 0);
void BalanceHP(int32 penalty, uint32 group_id, float range = 0, Mob* caster = nullptr, int32 limit = 0);
void BalanceMana(int32 penalty, uint32 group_id, float range = 0, Mob* caster = nullptr, int32 limit = 0);
void HealGroup(uint32 heal_amount, Mob* caster, uint32 group_id, float range = 0);
void SplitMoney(uint32 gid, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, Client *splitter = nullptr);
void TeleportGroup(Mob* sender, uint32 zoneID, uint16 instance_id, float x, float y, float z, float heading, uint32 gid);