Compare commits

...

10 commits

Author SHA1 Message Date
6454c737da added SQL migrations
Some checks are pending
nopch-build / ubuntu-22.04-clang-15-nopch (push) Waiting to run
nopch-build / ubuntu-24.04-clang-18-nopch (push) Waiting to run
nopch-build / ubuntu-24.04-gcc-14-nopch (push) Waiting to run
pch-build / ubuntu-22.04-clang-15-pch (push) Waiting to run
pch-build / ubuntu-24.04-clang-18-pch (push) Waiting to run
nopch-module-build / ubuntu-24.04-clang-18-nopch-modules (push) Waiting to run
Dashboard CI / Test Bash Scripts (push) Waiting to run
Dashboard CI / Test Bash Scripts-1 (push) Waiting to run
Dashboard CI / Build and Integration Test (push) Waiting to run
Dashboard CI / Build and Integration Test-1 (push) Waiting to run
docker-build / build-containers (push) Waiting to run
import-pending / import-pending (push) Waiting to run
macos-build / macos-14 (push) Waiting to run
tools / ubuntu-24.04-clang-18 (push) Waiting to run
windows-build / windows-latest (push) Waiting to run
2026-05-15 16:16:37 -06:00
e6af1065e4 increase both health and mana regen while sitting by double 2026-05-15 00:47:29 -06:00
be47269ede added a corpse summon spell... well copying contents of corpses, close enough. 2026-05-04 01:07:18 -06:00
a0b945afcd fixed xp reduction at max level 2026-05-03 02:40:01 -06:00
9715afee68 fixed gold dupping, empty corpses now reliably show as empty once opened 2026-05-03 02:19:13 -06:00
48a8d0c620 fixed corpses being marked as looted before they are fully looted 2026-05-02 20:22:42 -06:00
0629e81436 reworked corpse looting perms 2026-05-02 17:44:13 -06:00
9fb81f22c0 added corpse looting, also store player bags in db 2026-05-02 00:11:58 -06:00
70a5d07325 player items now get stored when they die, items are removed from player. corpses show as lootable 2026-05-01 15:47:56 -06:00
18987fe43d added mariadb support, on death loose 10% xp and respawn at bind point 2026-04-30 22:28:54 -06:00
10 changed files with 805 additions and 102 deletions

View file

@ -0,0 +1,45 @@
-- DB update 2026_04_12_00 -> 2026_05_15_00
--
CREATE TABLE lost_corpses (
lost_corpse_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
player_guid BIGINT UNSIGNED NOT NULL,
corpse_guid BIGINT UNSIGNED NULL,
map_id SMALLINT UNSIGNED NOT NULL,
zone_id SMALLINT UNSIGNED NOT NULL,
position_x FLOAT NOT NULL,
position_y FLOAT NOT NULL,
position_z FLOAT NOT NULL,
orientation FLOAT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
money BIGINT UNSIGNED DEFAULT 0,
active TINYINT(1) NOT NULL DEFAULT 1,
INDEX idx_player_guid (player_guid),
INDEX idx_corpse_guid (corpse_guid),
INDEX idx_map_zone (map_id, zone_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE lost_corpse_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
lost_corpse_id BIGINT UNSIGNED NOT NULL,
item_entry INT UNSIGNED NOT NULL,
count INT UNSIGNED NOT NULL DEFAULT 1,
randomPropertyId INT DEFAULT 0,
durability INT UNSIGNED DEFAULT 0,
enchantments TEXT,
looted TINYINT(1) NOT NULL DEFAULT 0,
CONSTRAINT fk_lost_corpse
FOREIGN KEY (lost_corpse_id)
REFERENCES lost_corpses(lost_corpse_id)
ON DELETE CASCADE,
INDEX idx_lost_corpse_id (lost_corpse_id),
INDEX idx_item_entry (item_entry)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE lost_corpses
ADD UNIQUE KEY uniq_lookup (player_guid, corpse_guid, created_at);
ALTER TABLE lost_corpses
ADD COLUMN money_collected TINYINT(1) NOT NULL DEFAULT 0;

View file

@ -0,0 +1,92 @@
-- DB update 2026_04_30_03 -> 2026_05_15_00
--
UPDATE creature_template
SET HealthModifier = HealthModifier * 3,
DamageModifier = DamageModifier * 3;
UPDATE player_xp_for_level
SET Experience = Experience * 1.5;
INSERT INTO creature_template (
entry, name, subname,
minlevel, maxlevel,
faction, npcflag, unit_class, type
) VALUES (
90000,
'Chromie',
'Timewalker Guide',
80, 80,
35, 0, 1, 7
);
INSERT INTO creature_template_model (
CreatureID,
CreatureDisplayID,
DisplayScale,
Probability
) VALUES (
90000,
24877,
1,
1
);
INSERT INTO creature (
id1,
map,
position_x,
position_y,
position_z,
orientation,
spawntimesecs,
wander_distance,
curhealth,
curmana,
MovementType
) VALUES (
90000,
0,
-8844.938,
617.47314,
95.53623,
5.0512466,
120,
0,
1,
0,
0
);
INSERT INTO creature (
id1,
map,
position_x,
position_y,
position_z,
orientation,
spawntimesecs,
wander_distance,
curhealth,
curmana,
MovementType
) VALUES (
90000,
1,
1546.3574,
-4447.2905,
12.005171,
1.002923,
120,
0,
1,
0,
0
);
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (90001, 'spell_custom_recover_corpse_items');
INSERT INTO trainer_spell VALUES (11, 90000, 10000, 0, 0, 0, 0, 0, 12, 0);
INSERT INTO trainer_spell VALUES (12, 90000, 10000, 0, 0, 0, 0, 0, 12, 0);
INSERT INTO trainer_spell VALUES (33, 90001, 10000, 0, 0, 0, 0, 0, 12, 0);
INSERT INTO trainer_spell VALUES (34, 90001, 10000, 0, 0, 0, 0, 0, 12, 0);

View file

@ -2089,7 +2089,7 @@ WaterBreath.Timer = 180000
# 0 - Disabled
#
EnableLowLevelRegenBoost = 1
EnableLowLevelRegenBoost = 0
#
# Rate.MoveSpeed.Player
@ -2146,8 +2146,8 @@ Rate.Talent.Pet = 1
# 1 - (Rate.Focus)
# 1 - (Rate.Energy)
Rate.Health = 1
Rate.Mana = 1
Rate.Health = 0.5
Rate.Mana = 0.5
Rate.Rage.Income = 1
Rate.Rage.Loss = 1
Rate.RunicPower.Income = 1

View file

@ -59,13 +59,38 @@ DatabaseWorkerPool<T>::DatabaseWorkerPool() :
{
WPFatal(mysql_thread_safe(), "Used MySQL library isn't thread-safe.");
bool isSupportClientDB = mysql_get_client_version() >= MIN_MYSQL_CLIENT_VERSION;
bool isSameClientDB = mysql_get_client_version() == MYSQL_VERSION_ID;
#ifdef MARIADB_BASE_VERSION
WPFatal(isSupportClientDB, "AzerothCore does not support MySQL versions below 8.0\n\nFound version: {} / {}. Server compiled with: {}.\nSearch the wiki for ACE00043 in Common Errors (https://www.azerothcore.org/wiki/common-errors#ace00043).",
mysql_get_client_info(), mysql_get_client_version(), MYSQL_VERSION_ID);
WPFatal(isSameClientDB, "Used MySQL library version ({} id {}) does not match the version id used to compile AzerothCore (id {}).\nSearch the wiki for ACE00046 in Common Errors (https://www.azerothcore.org/wiki/common-errors#ace00046).",
mysql_get_client_info(), mysql_get_client_version(), MYSQL_VERSION_ID);
// MariaDB path (skip MySQL 8 strict checks)
LOG_INFO("sql.sql", "MariaDB detected: {} (client version: {})",
mysql_get_client_info(),
mysql_get_client_version());
#else
// MySQL 8+ strict validation path
const uint32 clientVersion = mysql_get_client_version();
const uint32 compiledVersion = MYSQL_VERSION_ID;
bool isSupportClientDB = clientVersion >= MIN_MYSQL_CLIENT_VERSION;
bool isSameClientDB = clientVersion == compiledVersion;
WPFatal(isSupportClientDB,
"AzerothCore does not support MySQL versions below 8.0\n\n"
"Found version: {} / {}. Server compiled with: {}.\n"
"Search ACE00043 in Common Errors.",
mysql_get_client_info(),
clientVersion,
compiledVersion);
WPFatal(isSameClientDB,
"Used MySQL library version ({} id {}) does not match compiled version id ({}).\n"
"Search ACE00046 in Common Errors.",
mysql_get_client_info(),
clientVersion,
compiledVersion);
#endif
}
template <class T>
@ -417,9 +442,10 @@ bool DatabaseIncompatibleVersion(std::string const mysqlVersion)
template <class T>
uint32 DatabaseWorkerPool<T>::OpenConnections(InternalIndex type, uint8 numConnections)
{
LOG_INFO("sql.sql", "OpenConnections called for type {}", type);
for (uint8 i = 0; i < numConnections; ++i)
{
// Create the connection
auto connection = [&]
{
switch (type)
@ -433,26 +459,42 @@ uint32 DatabaseWorkerPool<T>::OpenConnections(InternalIndex type, uint8 numConne
}
}();
if (uint32 error = connection->Open())
uint32 error = connection->Open();
if (error)
{
// Failed to open a connection or invalid version, abort and cleanup
_queue->Cancel();
_connections[type].clear();
return error;
}
else if (DatabaseIncompatibleVersion(connection->GetServerInfo()))
bool incompatible = DatabaseIncompatibleVersion(connection->GetServerInfo());
#ifdef MARIADB_BASE_VERSION
// MariaDB: do NOT treat as fatal, but also do NOT blindly trust it
if (incompatible)
{
LOG_ERROR("sql.driver", "AzerothCore does not support MySQL versions below 8.0\n\nFound server version: {}. Server compiled with: {}.",
connection->GetServerInfo(), MYSQL_VERSION_ID);
LOG_WARN("sql.driver",
"MariaDB detected with non-standard version response: {}",
connection->GetServerInfo());
}
#else
if (incompatible)
{
LOG_ERROR("sql.driver",
"AzerothCore does not support MySQL versions below 8.0\n\n"
"Found server version: {}. Server compiled with: {}.",
connection->GetServerInfo(),
MYSQL_VERSION_ID);
_queue->Cancel();
_connections[type].clear();
return 1;
}
else
{
_connections[type].push_back(std::move(connection));
}
#endif
_connections[type].push_back(std::move(connection));
}
// Everything is fine
return 0;
}

View file

@ -129,13 +129,19 @@ uint32 MySQLConnection::Open()
if (m_connectionInfo.ssl != "")
{
mysql_ssl_mode opt_use_ssl = SSL_MODE_DISABLED;
if (m_connectionInfo.ssl == "ssl")
{
opt_use_ssl = SSL_MODE_REQUIRED;
}
#if defined(MARIADB_BASE_VERSION)
// MariaDB: no mysql_ssl_mode, skip
#else
// MySQL 8+
mysql_ssl_mode opt_use_ssl = SSL_MODE_DISABLED;
if (m_connectionInfo.ssl == "ssl")
{
opt_use_ssl = SSL_MODE_REQUIRED;
}
mysql_options(mysqlInit, MYSQL_OPT_SSL_MODE, (char const*)&opt_use_ssl);
mysql_options(mysqlInit, MYSQL_OPT_SSL_MODE, (char const*)&opt_use_ssl);
#endif
}
m_Mysql = reinterpret_cast<MySQLHandle*>(mysql_real_connect(mysqlInit, m_connectionInfo.host.c_str(), m_connectionInfo.user.c_str(),
@ -217,7 +223,13 @@ bool MySQLConnection::Execute(PreparedStatementBase* stmt)
uint32 _s = getMSTime();
#if MYSQL_VERSION_ID >= 80300
if (mysql_stmt_bind_named_param(msql_STMT, msql_BIND, m_mStmt->GetParameterCount(), nullptr))
#if defined(MARIADB_BASE_VERSION)
// MariaDB: only supports positional parameters
if (mysql_stmt_bind_param(msql_STMT, msql_BIND))
#else
// MySQL 8+: supports named parameters
if (mysql_stmt_bind_named_param(msql_STMT, msql_BIND, m_mStmt->GetParameterCount(), nullptr))
#endif
#else
if (mysql_stmt_bind_param(msql_STMT, msql_BIND))
#endif
@ -269,7 +281,13 @@ bool MySQLConnection::_Query(PreparedStatementBase* stmt, MySQLPreparedStatement
uint32 _s = getMSTime();
#if MYSQL_VERSION_ID >= 80300
if (mysql_stmt_bind_named_param(msql_STMT, msql_BIND, m_mStmt->GetParameterCount(), nullptr))
#if defined(MARIADB_BASE_VERSION)
// MariaDB: only supports positional parameters
if (mysql_stmt_bind_param(msql_STMT, msql_BIND))
#else
// MySQL 8+: supports named parameters
if (mysql_stmt_bind_named_param(msql_STMT, msql_BIND, m_mStmt->GetParameterCount(), nullptr))
#endif
#else
if (mysql_stmt_bind_param(msql_STMT, msql_BIND))
#endif

View file

@ -40,7 +40,9 @@
#include "ConditionMgr.h"
#include "Config.h"
#include "CreatureAI.h"
#include "DBCEnums.h"
#include "DatabaseEnv.h"
#include "DatabaseEnvFwd.h"
#include "DisableMgr.h"
#include "Formulas.h"
#include "GameEventMgr.h"
@ -60,6 +62,7 @@
#include "MapMgr.h"
#include "MiscPackets.h"
#include "ObjectAccessor.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "OutdoorPvP.h"
#include "OutdoorPvPMgr.h"
@ -1876,6 +1879,9 @@ void Player::Regenerate(Powers power)
addvalue += GetFloatValue(UNIT_FIELD_POWER_REGEN_INTERRUPTED_FLAT_MODIFIER + AsUnderlyingType(POWER_MANA)) * ManaIncreaseRate * 0.001f * m_regenTimer;
else
addvalue += GetFloatValue(UNIT_FIELD_POWER_REGEN_FLAT_MODIFIER + AsUnderlyingType(POWER_MANA)) * ManaIncreaseRate * 0.001f * m_regenTimer;
if (!IsStandState())
addvalue *= 2.0f;
}
break;
case POWER_RAGE: // Regenerate rage
@ -1999,7 +2005,7 @@ void Player::RegenerateHealth()
if (!IsStandState())
{
addvalue *= 1.33f;
addvalue *= 2.0f;
}
addvalue *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALTH_REGEN_PERCENT);
@ -4326,25 +4332,10 @@ void Player::DeleteOldRecoveryItems(uint32 keepDays)
*/
void Player::BuildPlayerRepop()
{
WorldPacket data(SMSG_PRE_RESURRECT, GetPackGUID().size());
data << GetPackGUID();
SendDirectMessage(&data);
if (getRace(true) == RACE_NIGHTELF)
{
CastSpell(this, 20584, true);
}
CastSpell(this, 8326, true);
// there must be SMSG.FORCE_RUN_SPEED_CHANGE, SMSG.FORCE_SWIM_SPEED_CHANGE, SMSG.MOVE_WATER_WALK
// there must be SMSG.STOP_MIRROR_TIMER
// the player cannot have a corpse already on current map, only bones which are not returned by GetCorpse
WorldLocation corpseLocation = GetCorpseLocation();
if (GetCorpse() && corpseLocation.GetMapId() == GetMapId())
{
LOG_ERROR("entities.player", "BuildPlayerRepop: player {} ({}) already has a corpse", GetName(), GetGUID().ToString());
return;
}
// Override -- Teleport player back to their bind point [ElderShell]
ResurrectPlayer(0.1f);
// NOTE: We teleport the player in Player::RepopAtGraveyard()
// End Override
// create a corpse and place it at the player's location
Corpse* corpse = CreateCorpse();
@ -4354,22 +4345,6 @@ void Player::BuildPlayerRepop()
return;
}
GetMap()->AddToMap(corpse);
SetHealth(1); // convert player body to ghost
SetWaterWalking(true);
if (!IsImmobilizedState())
SendMoveRoot(false);
RemoveUnitFlag(UNIT_FLAG_SKINNABLE); // BG - remove insignia related
int32 corpseReclaimDelay = CalculateCorpseReclaimDelay();
if (corpseReclaimDelay >= 0)
{
SendCorpseReclaimDelay(corpseReclaimDelay);
}
corpse->ResetGhostTime(); // to prevent cheating
StopMirrorTimers(); // disable timers on bars
SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_ANIM_TIER, UNIT_BYTE1_FLAG_ALWAYS_STAND); // set and clear other
sScriptMgr->OnPlayerReleasedGhost(this);
}
void Player::ResurrectPlayer(float restore_percent, bool applySickness)
@ -4498,7 +4473,7 @@ void Player::OfflineResurrect(ObjectGuid const& guid, CharacterDatabaseTransacti
Corpse* Player::CreateCorpse()
{
// prevent existence 2 corpse for player
SpawnCorpseBones();
SpawnCorpseBones(); // TODO: We'd love to allow for several corpses in the world per player but it might break things... [ElderShell]
uint32 _uf, _pb, _pb2, _cfb1, _cfb2;
@ -4530,7 +4505,7 @@ Corpse* Player::CreateCorpse()
corpse->SetUInt32Value(CORPSE_FIELD_BYTES_1, _cfb1);
corpse->SetUInt32Value(CORPSE_FIELD_BYTES_2, _cfb2);
uint32 flags = CORPSE_FLAG_UNK2;
uint32 flags = CORPSE_FLAG_UNK2 | CORPSE_FLAG_LOOTABLE;
if (HasPlayerFlag(PLAYER_FLAGS_HIDE_HELM))
flags |= CORPSE_FLAG_HIDE_HELM;
if (HasPlayerFlag(PLAYER_FLAGS_HIDE_CLOAK))
@ -4549,6 +4524,9 @@ Corpse* Player::CreateCorpse()
corpse->SetUInt32Value(CORPSE_FIELD_GUILD, GetGuildId());
corpse->lootRecipient = this;
corpse->SetUInt32Value(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE);
uint32 iDisplayID;
uint32 iIventoryType;
uint32 _cfi;
@ -4573,6 +4551,122 @@ Corpse* Player::CreateCorpse()
if (!GetMap()->IsBattlegroundOrArena())
corpse->SaveToDB();
uint64 playerGuid = GetGUID().GetCounter();
uint64 corpseGuid = corpse->GetGUID().GetCounter();
// Insert corpse
CharacterDatabase.DirectExecute(
"INSERT INTO lost_corpses "
"(player_guid, corpse_guid, map_id, zone_id, position_x, position_y, position_z, orientation, money) "
"VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {})",
playerGuid,
corpseGuid,
GetMapId(),
GetZoneId(),
GetPositionX(),
GetPositionY(),
GetPositionZ(),
GetOrientation(),
GetMoney()
);
QueryResult result = CharacterDatabase.Query(
"SELECT lost_corpse_id "
"FROM lost_corpses "
"WHERE player_guid = {} "
"ORDER BY lost_corpse_id DESC "
"LIMIT 1",
playerGuid);
if (!result)
return corpse;
Field* fields = result->Fetch();
uint32 lostCorpseId = fields[0].Get<uint32>();
auto insertItem = [&](Item* item)
{
if (!item)
return;
CharacterDatabase.Execute(
"INSERT INTO lost_corpse_items "
"(lost_corpse_id, item_entry, count, randomPropertyId, durability, enchantments, looted) "
"VALUES ({}, {}, {}, {}, {}, '{}', 0)",
lostCorpseId,
item->GetEntry(),
item->GetCount(),
item->GetItemRandomPropertyId(),
item->GetUInt32Value(ITEM_FIELD_DURABILITY),
""
);
};
// EQUIPPED ITEMS
for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i)
{
if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i))
insertItem(item);
}
// INVENTORY + BAGS
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
{
if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
insertItem(item);
}
// BAGS
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
if (Bag* b = GetBagByPos(bag))
{
insertItem(b);
for (uint32 slot = 0; slot < b->GetBagSize(); ++slot)
{
if (Item* item = b->GetItemByPos(slot))
insertItem(item);
}
}
}
// Remove items and bags from player
for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i)
{
if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i))
{
RemoveItem(INVENTORY_SLOT_BAG_0, i, true);
}
}
for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i)
{
if (Bag* bag = GetBagByPos(i))
{
for (uint32 slot = 0; slot < bag->GetBagSize(); ++slot)
{
if (Item* item = bag->GetItemByPos(slot))
{
bag->RemoveItem(slot, true);
}
}
}
}
CharacterDatabase.Execute(
"DELETE ii FROM item_instance ii "
"JOIN character_inventory ci ON ci.item = ii.guid "
"WHERE ci.guid = {}",
GetGUID().GetCounter());
CharacterDatabase.Execute(
"DELETE FROM character_inventory "
"WHERE guid = {}",
GetGUID().GetCounter());
SetMoney(0);
return corpse;
}
@ -4840,39 +4934,54 @@ void Player::RepopAtGraveyard()
SpawnCorpseBones();
}
GraveyardStruct const* ClosestGrave = nullptr;
// Special handle for battleground maps
if (Battleground* bg = GetBattleground())
ClosestGrave = bg->GetClosestGraveyard(this);
else
{
if (sBattlefieldMgr->GetBattlefieldToZoneId(GetZoneId()))
ClosestGrave = sBattlefieldMgr->GetBattlefieldToZoneId(GetZoneId())->GetClosestGraveyard(this);
else
ClosestGrave = sGraveyard->GetClosestGraveyard(this, GetTeamId());
}
// stop countdown until repop
m_deathTimer = 0;
// if no grave found, stay at the current location
// and don't show spirit healer location
if (ClosestGrave)
TeleportTo(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, GetOrientation());
uint32 nextLevelXp = GetUInt32Value(PLAYER_NEXT_LEVEL_XP);
if (GetLevel() == DEFAULT_MAX_LEVEL)
{
TeleportTo(ClosestGrave->Map, ClosestGrave->x, ClosestGrave->y, ClosestGrave->z, GetOrientation());
if (isDead()) // not send if alive, because it used in TeleportTo()
QueryResult xpResult = WorldDatabase.Query(
"SELECT Experience FROM player_xp_for_level WHERE Level = {} LIMIT 1",
GetLevel() - 1
);
if (xpResult)
{
WorldPacket data(SMSG_DEATH_RELEASE_LOC, 4 * 4); // show spirit healer position on minimap
data << ClosestGrave->Map;
data << ClosestGrave->x;
data << ClosestGrave->y;
data << ClosestGrave->z;
SendDirectMessage(&data);
Field* fields = xpResult->Fetch();
nextLevelXp = fields[0].Get<uint32>();
}
}
else if (GetPositionZ() < GetMap()->GetMinHeight(GetPositionX(), GetPositionY()))
TeleportTo(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, GetOrientation());
uint32 reduction = nextLevelXp / 10;
uint32 currentXp = GetUInt32Value(PLAYER_XP);
int32 xpToRemove = currentXp - reduction;
if (xpToRemove < 0)
{
uint8 currentLevel = GetLevel();
if (currentLevel > 1)
{
GiveLevel(currentLevel - 1);
nextLevelXp = GetUInt32Value(PLAYER_NEXT_LEVEL_XP);
SetUInt32Value(PLAYER_XP, nextLevelXp + xpToRemove);
}
else
{
SetUInt32Value(PLAYER_XP, 0);
}
}
else
{
SetUInt32Value(PLAYER_XP, currentXp - reduction);
}
//SetUInt32Value(PLAYER_XP, currentXp > reduction ? currentXp - reduction : 0);
RemovePlayerFlag(PLAYER_FLAGS_IS_OUT_OF_BOUNDS);
}
@ -7983,12 +8092,30 @@ void Player::SendLoot(ObjectGuid guid, LootType loot_type)
{
Corpse* bones = ObjectAccessor::GetCorpse(*this, guid);
if (!bones || !(loot_type == LOOT_CORPSE || loot_type == LOOT_INSIGNIA) || bones->GetType() != CORPSE_BONES || !bones->HasFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE))
if (!bones || !(loot_type == LOOT_CORPSE || loot_type == LOOT_INSIGNIA) || !bones->HasFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE))
{
SendLootRelease(guid);
return;
}
permission = NONE_PERMISSION;
ObjectGuid corpseGUID = bones->GetGUID();
CheckCorpseByGUID(corpseGUID);
if (Corpse* bones = ObjectAccessor::GetCorpse(*this, guid))
{
if (bones->GetOwnerGUID() != GetGUID())
{
SendLootError(guid, LOOT_ERROR_DIDNT_KILL);
return;
}
permission = OWNER_PERMISSION;
LoadLostCorpseLoot(bones->GetOwnerGUID(), bones->loot);
bones->loot.loot_type = LOOT_CORPSE;
}
loot = &bones->loot;
if (loot->loot_type == LOOT_NONE)
@ -8003,10 +8130,10 @@ void Player::SendLoot(ObjectGuid guid, LootType loot_type)
bones->loot.gold = uint32(urand(50, 150) * 0.016f * pow(float(pLevel) / 5.76f, 2.5f) * sWorld->getRate(RATE_DROP_MONEY));
}
if (bones->lootRecipient != this)
permission = NONE_PERMISSION;
else
permission = OWNER_PERMISSION;
//if (bones->lootRecipient != this)
// permission = NONE_PERMISSION;
//else
// permission = OWNER_PERMISSION;
}
else
{
@ -8192,6 +8319,154 @@ void Player::SendLoot(ObjectGuid guid, LootType loot_type)
SendLootError(guid, LOOT_ERROR_DIDNT_KILL);
}
void Player::LoadLostCorpseLoot(ObjectGuid playerGuid, Loot& loot)
{
loot.clear();
m_lostCorpseItems.clear();
if (playerGuid.IsEmpty())
return;
QueryResult corpseResult = CharacterDatabase.Query(
"SELECT lost_corpse_id, money, money_collected "
"FROM lost_corpses "
"WHERE player_guid = {} "
"AND SQRT(POW(position_x - {}, 2) + POW(position_y - {}, 2) + POW(position_z - {}, 2)) <= 5 "
"AND active = 1 "
"ORDER BY lost_corpse_id DESC LIMIT 1",
playerGuid.GetCounter(),
GetPositionX(),
GetPositionY(),
GetPositionZ()
);
if (!corpseResult)
return;
Field* corpseFields = corpseResult->Fetch();
uint32 lostCorpseId = corpseFields[0].Get<uint32>();
uint32 money = corpseFields[1].Get<uint32>();
bool moneyCollected = corpseFields[2].Get<uint8>();
if (!moneyCollected)
{
loot.gold = money;
}
QueryResult result = CharacterDatabase.Query(
"SELECT id, lost_corpse_id, item_entry, count, durability "
"FROM lost_corpse_items "
"WHERE lost_corpse_id = {} AND looted = 0 "
"ORDER BY id ASC",
lostCorpseId
);
if (!result)
return;
do
{
Field* fields = result->Fetch();
uint32 dbId = fields[0].Get<uint32>();
uint32 lostCorpseId = fields[1].Get<uint32>();
uint32 itemId = fields[2].Get<uint32>();
uint32 count = fields[3].Get<uint32>();
uint32 durability = fields[4].Get<uint32>();
LostCorpseItemData data;
data.id = dbId;
data.lostCorpseId = lostCorpseId;
data.itemId = itemId;
data.count = count;
data.durability = durability;
m_lostCorpseItems.push_back(data);
LootItem item;
item.itemid = itemId;
item.count = count;
loot.items.push_back(item);
} while (result->NextRow());
}
void Player::CheckCorpse(uint32 lostCorpseId, ObjectGuid corpseGuid)
{
// Check if any items remain unlooted
QueryResult remaining = CharacterDatabase.Query(
"SELECT COUNT(*) FROM lost_corpse_items "
"WHERE lost_corpse_id = {} AND looted = 0",
lostCorpseId
);
if (remaining)
{
uint32 count = remaining->Fetch()[0].Get<uint32>();
if (count == 0)
{
QueryResult moneyCheck = CharacterDatabase.Query(
"SELECT money_collected, money FROM lost_corpses "
"WHERE lost_corpse_id = {} OR money = 0",
lostCorpseId
);
if (moneyCheck)
{
bool moneyCollected = moneyCheck->Fetch()[0].Get<uint8>();
uint32 money = moneyCheck->Fetch()[1].Get<uint32>();
if (moneyCollected || money == 0)
{
// No items left, deactivate corpse
CharacterDatabase.Execute(
"UPDATE lost_corpses SET active = 0 WHERE lost_corpse_id = {}",
lostCorpseId
);
ObjectGuid guidToUse = corpseGuid.IsEmpty() ? GetLootGUID() : corpseGuid;
// Remove lootability in-game
if (Corpse* corpse = ObjectAccessor::GetCorpse(*this, guidToUse))
{
corpse->RemoveFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE);
}
}
}
}
}
}
void Player::CheckCorpseByGUID(ObjectGuid corpseGuid)
{
uint32 guid = corpseGuid.GetCounter();
uint32 playerGUID = GetGUID().GetCounter();
float player_x = GetPositionX();
float player_y = GetPositionY();
float player_z = GetPositionZ();
QueryResult result = CharacterDatabase.Query(
"SELECT lost_corpse_id "
"FROM lost_corpses "
"WHERE corpse_guid = {} AND player_guid = {} "
"AND SQRT(POW(position_x - {}, 2) + POW(position_y - {}, 2) + POW(position_z - {}, 2)) <= 5",
guid,
playerGUID,
player_x,
player_y,
player_z
);
if (result)
{
uint32 lostCorpseId = result->Fetch()[0].Get<uint32>();
CheckCorpse(lostCorpseId, corpseGuid);
}
}
void Player::SendLootError(ObjectGuid guid, LootError error)
{
WorldPacket data(SMSG_LOOT_RESPONSE, 10);
@ -13606,6 +13881,37 @@ LootItem* Player::StoreLootItem(uint8 lootSlot, Loot* loot, InventoryResult& msg
AllowedLooterSet looters = item->GetAllowedLooters();
Item* newitem = StoreNewItem(dest, item->itemid, true, item->randomPropertyId, looters);
// APPLY LOST CORPSE DURABILITY
if (newitem && lootSlot < m_lostCorpseItems.size())
{
LostCorpseItemData const& data = m_lostCorpseItems[lootSlot];
// Safety check (important if something desyncs)
if (data.itemId == item->itemid)
{
newitem->SetUInt32Value(ITEM_FIELD_DURABILITY, data.durability);
// clamp (prevents weird DB values breaking items)
if (newitem->GetUInt32Value(ITEM_FIELD_DURABILITY) > newitem->GetUInt32Value(ITEM_FIELD_MAXDURABILITY))
{
newitem->SetUInt32Value(
ITEM_FIELD_DURABILITY,
newitem->GetUInt32Value(ITEM_FIELD_MAXDURABILITY)
);
}
uint32 dbId = m_lostCorpseItems[lootSlot].id;
uint32 corpseId = m_lostCorpseItems[lootSlot].lostCorpseId;
CharacterDatabase.DirectExecute(
"UPDATE lost_corpse_items SET looted = 1 WHERE id = {}",
dbId
);
CheckCorpse(corpseId);
}
}
if (qitem)
{
qitem->is_looted = true;

View file

@ -1082,6 +1082,17 @@ struct PendingSpellCastRequest
class Player : public Unit, public GridObject<Player>
{
struct LostCorpseItemData
{
uint32 id;
uint32 lostCorpseId;
uint32 itemId;
uint32 count;
uint32 durability;
};
std::vector<LostCorpseItemData> m_lostCorpseItems;
friend class WorldSession;
friend class CinematicMgr;
friend void Item::AddToUpdateQueueOf(Player* player);
@ -1090,6 +1101,9 @@ public:
explicit Player(WorldSession* session);
~Player() override;
void CheckCorpse(uint32 lostCorpseId, ObjectGuid corpseGuid = ObjectGuid::Empty);
void CheckCorpseByGUID(ObjectGuid corpseGuid);
void CleanupsBeforeDelete(bool finalCleanup = true) override;
void AddToWorld() override;
@ -2586,6 +2600,7 @@ public:
[[nodiscard]] bool CanSeeTrainer(Creature const* creature) const;
private:
void LoadLostCorpseLoot(ObjectGuid playerGuid, Loot& loot);
[[nodiscard]] bool AnyVendorOptionAvailable(uint32 menuId, Creature const* creature) const;
public:
[[nodiscard]] uint32 GetChampioningFaction() const { return m_ChampioningFaction; }

View file

@ -17,12 +17,14 @@
#include "Corpse.h"
#include "Creature.h"
#include "DatabaseEnvFwd.h"
#include "GameObject.h"
#include "Group.h"
#include "LootItemStorage.h"
#include "LootMgr.h"
#include "Object.h"
#include "ObjectAccessor.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "Opcodes.h"
#include "Player.h"
@ -146,7 +148,46 @@ void WorldSession::HandleLootMoneyOpcode(WorldPacket& /*recvData*/)
loot = &bones->loot;
shareMoney = false;
}
else
{
player->SendLootError(guid, LOOT_ERROR_DIDNT_KILL);
break;
}
QueryResult result = CharacterDatabase.Query(
"SELECT lost_corpse_id, money "
"FROM lost_corpses "
"WHERE corpse_guid = {} AND player_guid = {} "
"AND active = 1 AND money_collected = 0 "
"AND SQRT(POW(position_x - {}, 2) + POW(position_y - {}, 2) + POW(position_z - {}, 2)) <= 5",
bones->GetGUID().GetCounter(),
player->GetGUID().GetCounter(),
player->GetPositionX(),
player->GetPositionY(),
player->GetPositionZ()
);
if (result)
{
Field* fields = result->Fetch();
uint32 lostCorpseId = fields[0].Get<uint32>();
uint32 totalMoney = fields[1].Get<uint32>();
loot->gold = totalMoney;
CharacterDatabase.Execute(
"UPDATE lost_corpses SET money_collected = 1 "
"WHERE lost_corpse_id = {}",
lostCorpseId
);
player->CheckCorpse(lostCorpseId);
}
else
{
loot->gold = 0;
}
break;
}
case HighGuid::Item:
@ -243,7 +284,7 @@ void WorldSession::HandleLootOpcode(WorldPacket& recvData)
recvData >> guid;
// Check possible cheat
if (!GetPlayer()->IsAlive() || !guid.IsCreatureOrVehicle())
if (!GetPlayer()->IsAlive())
return;
// interrupt cast
@ -342,12 +383,15 @@ void WorldSession::DoLootRelease(ObjectGuid lguid)
loot = &corpse->loot;
ObjectGuid corpseGUID = corpse->GetGUID();
player->CheckCorpseByGUID(corpseGUID);
// Xinef: Buggs client? (Opening loot after closing)
//if (loot->isLooted())
{
loot->clear();
corpse->RemoveFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE);
}
//{
// loot->clear();
// corpse->RemoveFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE);
//}
}
else if (lguid.IsItem())
{

View file

@ -0,0 +1,139 @@
#include "ScriptMgr.h"
#include "SpellScript.h"
#include "SpellAuraEffects.h"
#include "Player.h"
#include "ObjectMgr.h"
#include "DatabaseEnv.h"
enum CustomSpells
{
SPELL_RECOVER_CORPSE_ITEMS = 90001
};
// SPELL_RECOVER_CORPSE_ITEMS, ID 90001
class spell_custom_recover_corpse_items : public SpellScript
{
PrepareSpellScript(spell_custom_recover_corpse_items);
void HandleAfterCast()
{
sLog->outMessage("spells", LOG_LEVEL_INFO, "AfterCast triggered for spell %u by %s", GetSpellInfo()->Id, GetCaster()->GetName().c_str());
if (GetSpellInfo()->Id != SPELL_RECOVER_CORPSE_ITEMS)
return;
Player* target = GetCaster()->ToPlayer();
if (!target)
return;
uint64 playerGuid = target->GetGUID().GetCounter();
// Get latest active corpse
QueryResult corpseResult = CharacterDatabase.Query(
"SELECT lost_corpse_id, money, money_collected "
"FROM lost_corpses "
"WHERE player_guid = {} AND active = 1 "
"ORDER BY created_at DESC LIMIT 1",
playerGuid);
if (!corpseResult)
return;
uint64 corpseId = (*corpseResult)[0].Get<uint64>();
uint32 money = (*corpseResult)[1].Get<uint32>();
bool moneyCollected = (*corpseResult)[2].Get<uint8>();
if (!moneyCollected)
{
bool modMoney = target->ModifyMoney(money);
if (!modMoney)
{
return;
}
CharacterDatabase.DirectExecute(
"UPDATE lost_corpses SET money_collected = 1 "
"WHERE lost_corpse_id = {}",
corpseId
);
}
// Get unlooted items
QueryResult itemsResult = CharacterDatabase.Query(
"SELECT id, item_entry, count, durability "
"FROM lost_corpse_items "
"WHERE lost_corpse_id = {} AND looted = 0",
corpseId);
if (!itemsResult)
return;
do
{
Field* fields = itemsResult->Fetch();
uint64 rowId = fields[0].Get<uint64>();
uint32 itemId = fields[1].Get<uint32>();
uint32 count = fields[2].Get<uint32>();
uint32 dur = fields[3].Get<uint32>();
ItemPosCountVec dest;
InventoryResult msg = target->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, count);
// Inventory full, stop immediately
if (msg != EQUIP_ERR_OK)
break;
Item* item = target->StoreNewItem(dest, itemId, true);
if (item)
{
if (ItemTemplate const* proto = item->GetTemplate())
{
uint32 maxDur = proto->MaxDurability;
if (maxDur > 0)
{
item->SetUInt32Value(ITEM_FIELD_DURABILITY, dur);
item->SetUInt32Value(ITEM_FIELD_MAXDURABILITY, maxDur);
}
}
// Mark as looted only if successfully given
CharacterDatabase.DirectExecute(
"UPDATE lost_corpse_items SET looted = 1 WHERE id = {}",
rowId);
}
} while (itemsResult->NextRow());
// Check that all items were looted
QueryResult remaining = CharacterDatabase.Query(
"SELECT COUNT(*) FROM lost_corpse_items "
"WHERE lost_corpse_id = {} AND looted = 0",
corpseId
);
if (remaining)
{
uint32 count = remaining->Fetch()[0].Get<uint32>();
if (count == 0)
{
CharacterDatabase.Execute(
"UPDATE lost_corpses SET active = 0 WHERE lost_corpse_id = {}",
corpseId
);
}
}
}
void Register() override
{
AfterCast += SpellCastFn(spell_custom_recover_corpse_items::HandleAfterCast);
}
};
void AddSC_custom_spell_scripts()
{
RegisterSpellScript(spell_custom_recover_corpse_items);
}

View file

@ -29,6 +29,7 @@ void AddSC_warlock_spell_scripts();
void AddSC_warrior_spell_scripts();
void AddSC_quest_spell_scripts();
void AddSC_item_spell_scripts();
void AddSC_custom_spell_scripts();
// The name of this function should match:
// void Add${NameOfDirectory}Scripts()
@ -47,4 +48,5 @@ void AddSpellsScripts()
AddSC_warrior_spell_scripts();
AddSC_quest_spell_scripts();
AddSC_item_spell_scripts();
AddSC_custom_spell_scripts();
}