From 18987fe43d587259e8026d6cde5a807d01a77242 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Thu, 30 Apr 2026 22:28:54 -0600 Subject: [PATCH 01/10] added mariadb support, on death loose 10% xp and respawn at bind point --- .../apps/worldserver/worldserver.conf.dist | 6 +- .../database/Database/DatabaseWorkerPool.cpp | 76 ++++++++++++---- .../database/Database/MySQLConnection.cpp | 34 +++++-- src/server/game/Entities/Player/Player.cpp | 91 ++++++------------- 4 files changed, 118 insertions(+), 89 deletions(-) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 675f46974..c30e3fbbc 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -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 diff --git a/src/server/database/Database/DatabaseWorkerPool.cpp b/src/server/database/Database/DatabaseWorkerPool.cpp index ee19a5a48..832fe5ba7 100644 --- a/src/server/database/Database/DatabaseWorkerPool.cpp +++ b/src/server/database/Database/DatabaseWorkerPool.cpp @@ -59,13 +59,38 @@ DatabaseWorkerPool::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 @@ -417,9 +442,10 @@ bool DatabaseIncompatibleVersion(std::string const mysqlVersion) template uint32 DatabaseWorkerPool::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::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; } diff --git a/src/server/database/Database/MySQLConnection.cpp b/src/server/database/Database/MySQLConnection.cpp index 48c9c4a62..e409dd71b 100644 --- a/src/server/database/Database/MySQLConnection.cpp +++ b/src/server/database/Database/MySQLConnection.cpp @@ -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(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 diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 0739c1d3b..869278407 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -4326,25 +4326,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 +4339,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) @@ -4840,39 +4809,39 @@ 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); + uint32 reduction = nextLevelXp / 10; + + uint32 currentXp = GetUInt32Value(PLAYER_XP); + + int32 xpToRemove = currentXp - reduction; + + if (xpToRemove < 0) { - TeleportTo(ClosestGrave->Map, ClosestGrave->x, ClosestGrave->y, ClosestGrave->z, GetOrientation()); - if (isDead()) // not send if alive, because it used in TeleportTo() + uint8 currentLevel = GetLevel(); + + if (currentLevel > 1) { - 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); + GiveLevel(currentLevel - 1); + nextLevelXp = GetUInt32Value(PLAYER_NEXT_LEVEL_XP); + SetUInt32Value(PLAYER_XP, nextLevelXp + xpToRemove); + } + else + { + SetUInt32Value(PLAYER_XP, 0); } } - else if (GetPositionZ() < GetMap()->GetMinHeight(GetPositionX(), GetPositionY())) - TeleportTo(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, GetOrientation()); + else + { + SetUInt32Value(PLAYER_XP, currentXp - reduction); + } + + //SetUInt32Value(PLAYER_XP, currentXp > reduction ? currentXp - reduction : 0); RemovePlayerFlag(PLAYER_FLAGS_IS_OUT_OF_BOUNDS); } From 70a5d0732562dea4c60d8690079e04c577dd4968 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Fri, 1 May 2026 15:47:56 -0600 Subject: [PATCH 02/10] player items now get stored when they die, items are removed from player. corpses show as lootable --- src/server/game/Entities/Player/Player.cpp | 121 ++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 869278407..edc8e4065 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -4467,7 +4467,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; @@ -4499,7 +4499,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)) @@ -4518,6 +4518,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; @@ -4542,6 +4545,120 @@ 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(); + + 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)) + { + 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; } From 9fb81f22c0aa2864d173869bef6748e1313b1eb5 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Sat, 2 May 2026 00:11:58 -0600 Subject: [PATCH 03/10] added corpse looting, also store player bags in db --- src/server/game/Entities/Player/Player.cpp | 137 ++++++++++++++++++++- src/server/game/Entities/Player/Player.h | 12 ++ src/server/game/Handlers/LootHandler.cpp | 2 +- 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index edc8e4065..f448a9e6d 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -4615,6 +4615,8 @@ Corpse* Player::CreateCorpse() { if (Bag* b = GetBagByPos(bag)) { + insertItem(b); + for (uint32 slot = 0; slot < b->GetBagSize(); ++slot) { if (Item* item = b->GetItemByPos(slot)) @@ -8069,12 +8071,24 @@ 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; } + if (Corpse* bones = ObjectAccessor::GetCorpse(*this, guid)) + { + if (bones->lootRecipient != this) + { + SendLootError(guid, LOOT_ERROR_DIDNT_KILL); + return; + } + + LoadLostCorpseLoot(bones->GetOwnerGUID(), bones->loot); + bones->loot.loot_type = LOOT_CORPSE; + } + loot = &bones->loot; if (loot->loot_type == LOOT_NONE) @@ -8278,6 +8292,71 @@ 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 " + "FROM lost_corpses " + "WHERE player_guid = {} " + "ORDER BY lost_corpse_id DESC LIMIT 1", + playerGuid.GetCounter() + ); + + if (!corpseResult) + return; + + Field* corpseFields = corpseResult->Fetch(); + + uint32 lostCorpseId = corpseFields[0].Get(); + uint32 money = corpseFields[1].Get(); + + 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 lostCorpseId = fields[1].Get(); + uint32 itemId = fields[2].Get(); + uint32 count = fields[3].Get(); + uint32 durability = fields[4].Get(); + + 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::SendLootError(ObjectGuid guid, LootError error) { WorldPacket data(SMSG_LOOT_RESPONSE, 10); @@ -13692,6 +13771,62 @@ 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 + ); + + // Check if any items remain unlooted + 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(); + + if (count == 0) + { + // No items left, deactivate corpse + CharacterDatabase.Execute( + "UPDATE lost_corpses SET active = 0 WHERE lost_corpse_id = {}", + corpseId + ); + + // Remove lootability in-game + if (Corpse* corpse = ObjectAccessor::GetCorpse(*this, GetLootGUID())) + { + corpse->RemoveFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE); + } + } + } + } + } + if (qitem) { qitem->is_looted = true; diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 2eb10a110..8b5d358a5 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1082,6 +1082,17 @@ struct PendingSpellCastRequest class Player : public Unit, public GridObject { + struct LostCorpseItemData + { + uint32 id; + uint32 lostCorpseId; + uint32 itemId; + uint32 count; + uint32 durability; + }; + + std::vector m_lostCorpseItems; + friend class WorldSession; friend class CinematicMgr; friend void Item::AddToUpdateQueueOf(Player* player); @@ -2586,6 +2597,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; } diff --git a/src/server/game/Handlers/LootHandler.cpp b/src/server/game/Handlers/LootHandler.cpp index 50636f27c..36165450e 100644 --- a/src/server/game/Handlers/LootHandler.cpp +++ b/src/server/game/Handlers/LootHandler.cpp @@ -243,7 +243,7 @@ void WorldSession::HandleLootOpcode(WorldPacket& recvData) recvData >> guid; // Check possible cheat - if (!GetPlayer()->IsAlive() || !guid.IsCreatureOrVehicle()) + if (!GetPlayer()->IsAlive()) return; // interrupt cast From 0629e8143659cd52d2b4d2b581be481b69949d80 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Sat, 2 May 2026 17:44:13 -0600 Subject: [PATCH 04/10] reworked corpse looting perms --- src/server/game/Entities/Player/Player.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index f448a9e6d..8ddb5926c 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -4616,7 +4616,7 @@ Corpse* Player::CreateCorpse() if (Bag* b = GetBagByPos(bag)) { insertItem(b); - + for (uint32 slot = 0; slot < b->GetBagSize(); ++slot) { if (Item* item = b->GetItemByPos(slot)) @@ -8077,13 +8077,16 @@ void Player::SendLoot(ObjectGuid guid, LootType loot_type) return; } + permission = NONE_PERMISSION; + if (Corpse* bones = ObjectAccessor::GetCorpse(*this, guid)) { - if (bones->lootRecipient != this) + 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; @@ -8103,10 +8106,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 { From 48a8d0c6208d7a94669b29f8882a927efd980141 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Sat, 2 May 2026 20:22:42 -0600 Subject: [PATCH 05/10] fixed corpses being marked as looted before they are fully looted --- src/server/game/Handlers/LootHandler.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/game/Handlers/LootHandler.cpp b/src/server/game/Handlers/LootHandler.cpp index 36165450e..8a32702ba 100644 --- a/src/server/game/Handlers/LootHandler.cpp +++ b/src/server/game/Handlers/LootHandler.cpp @@ -344,10 +344,10 @@ void WorldSession::DoLootRelease(ObjectGuid lguid) // 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()) { From 9715afee6841d4fb8cbd62c6ee5ef754227db78d Mon Sep 17 00:00:00 2001 From: ElderShell Date: Sun, 3 May 2026 02:19:13 -0600 Subject: [PATCH 06/10] fixed gold dupping, empty corpses now reliably show as empty once opened --- src/server/game/Entities/Player/Player.cpp | 121 ++++++++++++++++----- src/server/game/Entities/Player/Player.h | 3 + src/server/game/Handlers/LootHandler.cpp | 44 ++++++++ 3 files changed, 139 insertions(+), 29 deletions(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 8ddb5926c..7973b0193 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -41,6 +41,7 @@ #include "Config.h" #include "CreatureAI.h" #include "DatabaseEnv.h" +#include "DatabaseEnvFwd.h" #include "DisableMgr.h" #include "Formulas.h" #include "GameEventMgr.h" @@ -60,6 +61,7 @@ #include "MapMgr.h" #include "MiscPackets.h" #include "ObjectAccessor.h" +#include "ObjectGuid.h" #include "ObjectMgr.h" #include "OutdoorPvP.h" #include "OutdoorPvPMgr.h" @@ -8079,6 +8081,9 @@ void Player::SendLoot(ObjectGuid guid, LootType loot_type) permission = NONE_PERMISSION; + ObjectGuid corpseGUID = bones->GetGUID(); + CheckCorpseByGUID(corpseGUID); + if (Corpse* bones = ObjectAccessor::GetCorpse(*this, guid)) { if (bones->GetOwnerGUID() != GetGUID()) @@ -8304,11 +8309,16 @@ void Player::LoadLostCorpseLoot(ObjectGuid playerGuid, Loot& loot) return; QueryResult corpseResult = CharacterDatabase.Query( - "SELECT lost_corpse_id, money " + "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() + playerGuid.GetCounter(), + GetPositionX(), + GetPositionY(), + GetPositionZ() ); if (!corpseResult) @@ -8318,8 +8328,12 @@ void Player::LoadLostCorpseLoot(ObjectGuid playerGuid, Loot& loot) uint32 lostCorpseId = corpseFields[0].Get(); uint32 money = corpseFields[1].Get(); + bool moneyCollected = corpseFields[2].Get(); - loot.gold = money; + if (!moneyCollected) + { + loot.gold = money; + } QueryResult result = CharacterDatabase.Query( "SELECT id, lost_corpse_id, item_entry, count, durability " @@ -8360,6 +8374,80 @@ void Player::LoadLostCorpseLoot(ObjectGuid playerGuid, Loot& loot) } 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(); + + 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(); + uint32 money = moneyCheck->Fetch()[1].Get(); + + 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(); + CheckCorpse(lostCorpseId, corpseGuid); + } +} + void Player::SendLootError(ObjectGuid guid, LootError error) { WorldPacket data(SMSG_LOOT_RESPONSE, 10); @@ -13801,32 +13889,7 @@ LootItem* Player::StoreLootItem(uint8 lootSlot, Loot* loot, InventoryResult& msg dbId ); - // Check if any items remain unlooted - 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(); - - if (count == 0) - { - // No items left, deactivate corpse - CharacterDatabase.Execute( - "UPDATE lost_corpses SET active = 0 WHERE lost_corpse_id = {}", - corpseId - ); - - // Remove lootability in-game - if (Corpse* corpse = ObjectAccessor::GetCorpse(*this, GetLootGUID())) - { - corpse->RemoveFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE); - } - } - } + CheckCorpse(corpseId); } } diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 8b5d358a5..574b08cd3 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1101,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; diff --git a/src/server/game/Handlers/LootHandler.cpp b/src/server/game/Handlers/LootHandler.cpp index 8a32702ba..11568d1b4 100644 --- a/src/server/game/Handlers/LootHandler.cpp +++ b/src/server/game/Handlers/LootHandler.cpp @@ -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 totalMoney = fields[1].Get(); + + 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: @@ -342,6 +383,9 @@ 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()) //{ From a0b945afcdc8053172fab5fcaf3bd1d09828de50 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Sun, 3 May 2026 02:40:01 -0600 Subject: [PATCH 07/10] fixed xp reduction at max level --- src/server/game/Entities/Player/Player.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 7973b0193..7579a6f7c 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -40,6 +40,7 @@ #include "ConditionMgr.h" #include "Config.h" #include "CreatureAI.h" +#include "DBCEnums.h" #include "DatabaseEnv.h" #include "DatabaseEnvFwd.h" #include "DisableMgr.h" @@ -4936,6 +4937,21 @@ void Player::RepopAtGraveyard() TeleportTo(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, GetOrientation()); uint32 nextLevelXp = GetUInt32Value(PLAYER_NEXT_LEVEL_XP); + + if (GetLevel() == DEFAULT_MAX_LEVEL) + { + QueryResult xpResult = WorldDatabase.Query( + "SELECT Experience FROM player_xp_for_level WHERE Level = {} LIMIT 1", + GetLevel() - 1 + ); + + if (xpResult) + { + Field* fields = xpResult->Fetch(); + nextLevelXp = fields[0].Get(); + } + } + uint32 reduction = nextLevelXp / 10; uint32 currentXp = GetUInt32Value(PLAYER_XP); From be47269ede7d79219160eb84d3bb85eaa3835677 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Mon, 4 May 2026 01:07:18 -0600 Subject: [PATCH 08/10] added a corpse summon spell... well copying contents of corpses, close enough. --- src/server/scripts/Spells/spell_custom.cpp | 139 ++++++++++++++++++ .../scripts/Spells/spells_script_loader.cpp | 2 + 2 files changed, 141 insertions(+) create mode 100644 src/server/scripts/Spells/spell_custom.cpp diff --git a/src/server/scripts/Spells/spell_custom.cpp b/src/server/scripts/Spells/spell_custom.cpp new file mode 100644 index 000000000..db244006a --- /dev/null +++ b/src/server/scripts/Spells/spell_custom.cpp @@ -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(); + uint32 money = (*corpseResult)[1].Get(); + bool moneyCollected = (*corpseResult)[2].Get(); + + 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(); + uint32 itemId = fields[1].Get(); + uint32 count = fields[2].Get(); + uint32 dur = fields[3].Get(); + + 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(); + + 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); +} diff --git a/src/server/scripts/Spells/spells_script_loader.cpp b/src/server/scripts/Spells/spells_script_loader.cpp index ac6a6b2db..b9892970d 100644 --- a/src/server/scripts/Spells/spells_script_loader.cpp +++ b/src/server/scripts/Spells/spells_script_loader.cpp @@ -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(); } From e6af1065e4c544f658576a7e662469843cee2a62 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Fri, 15 May 2026 00:47:29 -0600 Subject: [PATCH 09/10] increase both health and mana regen while sitting by double --- src/server/game/Entities/Player/Player.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 7579a6f7c..0d8d08f01 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -1879,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 @@ -2002,7 +2005,7 @@ void Player::RegenerateHealth() if (!IsStandState()) { - addvalue *= 1.33f; + addvalue *= 2.0f; } addvalue *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALTH_REGEN_PERCENT); From 6454c737da7d415145753f07af463f60c2e58081 Mon Sep 17 00:00:00 2001 From: ElderShell Date: Fri, 15 May 2026 16:16:37 -0600 Subject: [PATCH 10/10] added SQL migrations --- .../updates/db_characters/2026_05_15_00.sql | 45 +++++++++ data/sql/updates/db_world/2026_05_15_00.sql | 92 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 data/sql/updates/db_characters/2026_05_15_00.sql create mode 100644 data/sql/updates/db_world/2026_05_15_00.sql diff --git a/data/sql/updates/db_characters/2026_05_15_00.sql b/data/sql/updates/db_characters/2026_05_15_00.sql new file mode 100644 index 000000000..5bcdbeee0 --- /dev/null +++ b/data/sql/updates/db_characters/2026_05_15_00.sql @@ -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; diff --git a/data/sql/updates/db_world/2026_05_15_00.sql b/data/sql/updates/db_world/2026_05_15_00.sql new file mode 100644 index 000000000..d2a2ae2cc --- /dev/null +++ b/data/sql/updates/db_world/2026_05_15_00.sql @@ -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);