/* * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "InstanceLockMgr.h" #include "DatabaseEnv.h" #include "DB2Stores.h" #include "Errors.h" #include "GameTime.h" #include "Log.h" #include "Map.h" // for TransferAbortReason #include "MapManager.h" #include "World.h" InstanceLockData::InstanceLockData() = default; InstanceLockData::~InstanceLockData() = default; InstanceLock::InstanceLock(uint32 mapId, Difficulty difficultyId, InstanceResetTimePoint expiryTime, uint32 instanceId) : _mapId(mapId), _difficultyId(difficultyId), _instanceId(instanceId), _expiryTime(expiryTime), _extended(false), _isInUse(false), _isNew(false) { } InstanceLock::~InstanceLock() = default; bool InstanceLock::IsExpired() const { return _expiryTime < GameTime::GetSystemTime(); } InstanceResetTimePoint InstanceLock::GetEffectiveExpiryTime() const { if (!IsExtended()) return GetExpiryTime(); MapDb2Entries entries{ _mapId, _difficultyId }; // return next reset time if (IsExpired()) return sInstanceLockMgr.GetNextResetTime(entries); // if not expired, return expiration time + 1 reset period return GetExpiryTime() + Seconds(entries.MapDifficulty->GetRaidDuration()); } SharedInstanceLockData::SharedInstanceLockData() = default; SharedInstanceLockData::~SharedInstanceLockData() { // Cleanup database if (InstanceId) sInstanceLockMgr.OnSharedInstanceLockDataDelete(InstanceId); } SharedInstanceLock::SharedInstanceLock(uint32 mapId, Difficulty difficultyId, InstanceResetTimePoint expiryTime, uint32 instanceId, std::shared_ptr sharedData) : InstanceLock(mapId, difficultyId, expiryTime, instanceId), _sharedData(std::move(sharedData)) { } MapDb2Entries::MapDb2Entries(uint32 mapId, Difficulty difficulty) : Map(sMapStore.AssertEntry(mapId)), MapDifficulty(ASSERT_NOTNULL(sDB2Manager.GetMapDifficultyData(mapId, difficulty))) { } MapDb2Entries::MapDb2Entries(MapEntry const* map, MapDifficultyEntry const* mapDifficulty) : Map(map), MapDifficulty(mapDifficulty) { } InstanceLockKey MapDb2Entries::GetKey() const { return { MapDifficulty->MapID, MapDifficulty->LockID }; } bool MapDb2Entries::IsInstanceIdBound() const { return !Map->IsFlexLocking() && !MapDifficulty->IsUsingEncounterLocks(); } InstanceLockUpdateEvent::InstanceLockUpdateEvent(InstanceLockUpdateEvent&& other) noexcept = default; InstanceLockUpdateEvent& InstanceLockUpdateEvent::operator=(InstanceLockUpdateEvent&&) noexcept = default; InstanceLockUpdateEvent::~InstanceLockUpdateEvent() = default; InstanceLockMgr::InstanceLockMgr() = default; InstanceLockMgr::~InstanceLockMgr() = default; void InstanceLockMgr::Load() { std::unordered_map> instanceLockDataById; // 0 1 2 if (QueryResult result = CharacterDatabase.Query("SELECT instanceId, data, completedEncountersMask FROM instance")) { do { Field* fields = result->Fetch(); uint32 instanceId = fields[0].GetUInt32(); std::shared_ptr data = std::make_shared(); data->Data = fields[1].GetString(); data->CompletedEncountersMask = fields[2].GetUInt32(); data->InstanceId = instanceId; instanceLockDataById[instanceId] = std::move(data); } while (result->NextRow()); } // ORDER BY required by MapManager::RegisterInstanceId // 0 1 2 3 4 5 6 7 8 if (QueryResult result = CharacterDatabase.Query("SELECT guid, mapId, lockId, instanceId, difficulty, data, completedEncountersMask, expiryTime, extended FROM character_instance_lock ORDER BY instanceId")) { do { Field* fields = result->Fetch(); ObjectGuid playerGuid = ObjectGuid::Create(fields[0].GetUInt64()); uint32 mapId = fields[1].GetUInt32(); uint32 lockId = fields[2].GetUInt32(); uint32 instanceId = fields[3].GetUInt32(); Difficulty difficulty = Difficulty(fields[4].GetUInt8()); InstanceResetTimePoint expiryTime = std::chrono::system_clock::from_time_t(time_t(fields[7].GetUInt64())); // Mark instance id as being used sMapMgr->RegisterInstanceId(instanceId); InstanceLock* instanceLock; if (MapDb2Entries{ mapId, difficulty }.IsInstanceIdBound()) { auto sharedDataItr = instanceLockDataById.find(instanceId); if (sharedDataItr == instanceLockDataById.end()) { TC_LOG_ERROR("instance.locks", "Missing instance data for instance id based lock (id {})", instanceId); CharacterDatabase.PExecute("DELETE FROM character_instance_lock WHERE instanceId = {}", instanceId); continue; } instanceLock = new SharedInstanceLock(mapId, difficulty, expiryTime, instanceId, sharedDataItr->second); _instanceLockDataById[instanceId] = sharedDataItr->second; } else instanceLock = new InstanceLock(mapId, difficulty, expiryTime, instanceId); instanceLock->GetData()->Data = fields[5].GetString(); instanceLock->GetData()->CompletedEncountersMask = fields[6].GetUInt32(); instanceLock->SetExtended(fields[8].GetBool()); _instanceLocksByPlayer[playerGuid][InstanceLockKey{ mapId, lockId }].reset(instanceLock); } while (result->NextRow()); } } void InstanceLockMgr::Unload() { _unloading = true; _instanceLocksByPlayer.clear(); _instanceLockDataById.clear(); } TransferAbortReason InstanceLockMgr::CanJoinInstanceLock(ObjectGuid const& playerGuid, MapDb2Entries const& entries, InstanceLock const* instanceLock) const { InstanceLock const* playerInstanceLock = FindActiveInstanceLock(playerGuid, entries); if (!playerInstanceLock) return TRANSFER_ABORT_NONE; if (entries.Map->IsFlexLocking()) { // compare completed encounters - if instance has any encounters unkilled in players lock then cannot enter if (playerInstanceLock->GetData()->CompletedEncountersMask & ~instanceLock->GetData()->CompletedEncountersMask) return TRANSFER_ABORT_ALREADY_COMPLETED_ENCOUNTER; return TRANSFER_ABORT_NONE; } if (!entries.MapDifficulty->IsUsingEncounterLocks() && !playerInstanceLock->IsNew() && playerInstanceLock->GetInstanceId() != instanceLock->GetInstanceId()) return TRANSFER_ABORT_LOCKED_TO_DIFFERENT_INSTANCE; return TRANSFER_ABORT_NONE; } InstanceLock* InstanceLockMgr::FindInstanceLock(LockMap const& locks, ObjectGuid const& playerGuid, MapDb2Entries const& entries) { auto playerLocksItr = locks.find(playerGuid); if (playerLocksItr == locks.end()) return nullptr; auto lockItr = playerLocksItr->second.find(entries.GetKey()); if (lockItr == playerLocksItr->second.end()) return nullptr; return lockItr->second.get(); } InstanceLock* InstanceLockMgr::FindActiveInstanceLock(ObjectGuid const& playerGuid, MapDb2Entries const& entries) const { return FindActiveInstanceLock(playerGuid, entries, false, true); } InstanceLock* InstanceLockMgr::FindActiveInstanceLock(ObjectGuid const& playerGuid, MapDb2Entries const& entries, bool ignoreTemporary, bool ignoreExpired) const { if (!entries.MapDifficulty->HasResetSchedule()) return nullptr; std::shared_lock guard(_locksMutex); InstanceLock* lock = FindInstanceLock(_instanceLocksByPlayer, playerGuid, entries); // Ignore expired and not extended locks if (lock && (!lock->IsExpired() || lock->IsExtended() || !ignoreExpired)) return lock; if (ignoreTemporary) return nullptr; return FindInstanceLock(_temporaryInstanceLocksByPlayer, playerGuid, entries); } // used in world update thread (THREADUNSAFE packets) - no locking neccessary std::vector InstanceLockMgr::GetInstanceLocksForPlayer(ObjectGuid const& playerGuid) const { std::vector locks; auto playerLocksItr = _instanceLocksByPlayer.find(playerGuid); if (playerLocksItr != _instanceLocksByPlayer.end()) { locks.reserve(playerLocksItr->second.size()); std::transform(playerLocksItr->second.begin(), playerLocksItr->second.end(), std::back_inserter(locks), [](PlayerLockMap::value_type const& p) { return p.second.get(); }); } return locks; } // used in world update thread (cross map teleportation) - no locking neccessary InstanceLock* InstanceLockMgr::CreateInstanceLockForNewInstance(ObjectGuid const& playerGuid, MapDb2Entries const& entries, uint32 instanceId) { if (!entries.MapDifficulty->HasResetSchedule()) return nullptr; InstanceLock* instanceLock; if (entries.IsInstanceIdBound()) { std::shared_ptr sharedData = std::make_shared(); _instanceLockDataById[instanceId] = sharedData; instanceLock = new SharedInstanceLock(entries.MapDifficulty->MapID, Difficulty(entries.MapDifficulty->DifficultyID), GetNextResetTime(entries), instanceId, std::move(sharedData)); } else instanceLock = new InstanceLock(entries.MapDifficulty->MapID, Difficulty(entries.MapDifficulty->DifficultyID), GetNextResetTime(entries), instanceId); instanceLock->SetIsNew(true); _temporaryInstanceLocksByPlayer[playerGuid][entries.GetKey()].reset(instanceLock); TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Created new temporary instance lock for {} in instance {}", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), instanceId); return instanceLock; } InstanceLock* InstanceLockMgr::UpdateInstanceLockForPlayer(CharacterDatabaseTransaction trans, ObjectGuid const& playerGuid, MapDb2Entries const& entries, InstanceLockUpdateEvent&& updateEvent) { InstanceLock* instanceLock = FindActiveInstanceLock(playerGuid, entries, true, true); if (!instanceLock) { std::unique_lock guard(_locksMutex); // Move lock from temporary storage if it exists there // This is to avoid destroying expired locks before any boss is killed in a fresh lock // player can still change his mind, exit instance and reactivate old lock auto playerLocksItr = _temporaryInstanceLocksByPlayer.find(playerGuid); if (playerLocksItr != _temporaryInstanceLocksByPlayer.end()) { auto lockItr = playerLocksItr->second.find(entries.GetKey()); if (lockItr != playerLocksItr->second.end()) { instanceLock = lockItr->second.release(); _instanceLocksByPlayer[playerGuid][entries.GetKey()].reset(instanceLock); playerLocksItr->second.erase(lockItr); if (playerLocksItr->second.empty()) _temporaryInstanceLocksByPlayer.erase(playerLocksItr); TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Promoting temporary lock to permanent for {} in instance {}", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), updateEvent.InstanceId); } } } if (!instanceLock) { if (entries.IsInstanceIdBound()) { auto sharedDataItr = _instanceLockDataById.find(updateEvent.InstanceId); ASSERT(sharedDataItr != _instanceLockDataById.end()); ASSERT(!sharedDataItr->second.expired()); instanceLock = new SharedInstanceLock(entries.MapDifficulty->MapID, Difficulty(entries.MapDifficulty->DifficultyID), GetNextResetTime(entries), updateEvent.InstanceId, sharedDataItr->second.lock()); ASSERT(static_cast(instanceLock)->GetSharedData()->InstanceId == updateEvent.InstanceId); } else instanceLock = new InstanceLock(entries.MapDifficulty->MapID, Difficulty(entries.MapDifficulty->DifficultyID), GetNextResetTime(entries), updateEvent.InstanceId); { std::unique_lock guard(_locksMutex); _instanceLocksByPlayer[playerGuid][entries.GetKey()].reset(instanceLock); } TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Created new instance lock for {} in instance {}", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), updateEvent.InstanceId); } else { if (entries.IsInstanceIdBound()) { ASSERT(instanceLock->GetInstanceId() == updateEvent.InstanceId); auto sharedDataItr = _instanceLockDataById.find(updateEvent.InstanceId); ASSERT(sharedDataItr != _instanceLockDataById.end()); ASSERT(sharedDataItr->second.lock().get() == static_cast(instanceLock)->GetSharedData()); } instanceLock->SetInstanceId(updateEvent.InstanceId); } instanceLock->SetIsNew(false); instanceLock->GetData()->Data = std::move(updateEvent.NewData); if (updateEvent.CompletedEncounter) { instanceLock->GetData()->CompletedEncountersMask |= 1u << updateEvent.CompletedEncounter->Bit; TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Instance lock for {} in instance {} gains completed encounter [{}-{}]", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), updateEvent.InstanceId, updateEvent.CompletedEncounter->ID, updateEvent.CompletedEncounter->Name[sWorld->GetDefaultDbcLocale()]); } // Synchronize map completed encounters into players completed encounters for UI if (!entries.MapDifficulty->IsUsingEncounterLocks()) instanceLock->GetData()->CompletedEncountersMask |= updateEvent.InstanceCompletedEncountersMask; if (updateEvent.EntranceWorldSafeLocId) instanceLock->GetData()->EntranceWorldSafeLocId = *updateEvent.EntranceWorldSafeLocId; if (instanceLock->IsExpired()) { instanceLock->SetExpiryTime(GetNextResetTime(entries)); instanceLock->SetExtended(false); TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Expired instance lock for {} in instance {} is now active", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), updateEvent.InstanceId); } CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_INSTANCE_LOCK); stmt->setUInt64(0, playerGuid.GetCounter()); stmt->setUInt32(1, entries.MapDifficulty->MapID); stmt->setUInt32(2, entries.MapDifficulty->LockID); trans->Append(stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_INSTANCE_LOCK); stmt->setUInt64(0, playerGuid.GetCounter()); stmt->setUInt32(1, entries.MapDifficulty->MapID); stmt->setUInt32(2, entries.MapDifficulty->LockID); stmt->setUInt32(3, instanceLock->GetInstanceId()); stmt->setUInt8(4, entries.MapDifficulty->DifficultyID); stmt->setString(5, instanceLock->GetData()->Data); stmt->setUInt32(6, instanceLock->GetData()->CompletedEncountersMask); stmt->setUInt32(7, instanceLock->GetData()->EntranceWorldSafeLocId); stmt->setUInt64(8, uint64(std::chrono::system_clock::to_time_t(instanceLock->GetExpiryTime()))); stmt->setUInt8(9, instanceLock->IsExtended() ? 1 : 0); trans->Append(stmt); return instanceLock; } void InstanceLockMgr::UpdateSharedInstanceLock(CharacterDatabaseTransaction trans, InstanceLockUpdateEvent&& updateEvent) { auto sharedDataItr = _instanceLockDataById.find(updateEvent.InstanceId); ASSERT(sharedDataItr != _instanceLockDataById.end()); std::shared_ptr sharedData = sharedDataItr->second.lock(); ASSERT(sharedData); ASSERT(!sharedData->InstanceId || sharedData->InstanceId == updateEvent.InstanceId); sharedData->Data = std::move(updateEvent.NewData); sharedData->InstanceId = updateEvent.InstanceId; if (updateEvent.CompletedEncounter) { sharedData->CompletedEncountersMask |= 1u << updateEvent.CompletedEncounter->Bit; TC_LOG_DEBUG("instance.locks", "Instance {} gains completed encounter [{}-{}]", updateEvent.InstanceId, updateEvent.CompletedEncounter->ID, updateEvent.CompletedEncounter->Name[sWorld->GetDefaultDbcLocale()]); } if (updateEvent.EntranceWorldSafeLocId) sharedData->EntranceWorldSafeLocId = *updateEvent.EntranceWorldSafeLocId; CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INSTANCE); stmt->setUInt32(0, sharedData->InstanceId); trans->Append(stmt); stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_INSTANCE); stmt->setUInt32(0, sharedData->InstanceId); stmt->setString(1, sharedData->Data); stmt->setUInt32(2, sharedData->CompletedEncountersMask); stmt->setUInt32(3, sharedData->EntranceWorldSafeLocId); trans->Append(stmt); } void InstanceLockMgr::OnSharedInstanceLockDataDelete(uint32 instanceId) { if (_unloading) return; auto itr = _instanceLockDataById.find(instanceId); if (itr == _instanceLockDataById.end()) return; // weak_ptr state must be checked as well, it might be pointing to a different and valid lock data if (!itr->second.expired()) return; _instanceLockDataById.erase(itr); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INSTANCE); stmt->setUInt32(0, instanceId); CharacterDatabase.Execute(stmt); TC_LOG_DEBUG("instance.locks", "Deleting instance {} as it is no longer referenced by any player", instanceId); } std::pair InstanceLockMgr::UpdateInstanceLockExtensionForPlayer(ObjectGuid const& playerGuid, MapDb2Entries const& entries, bool extended) { if (InstanceLock* instanceLock = FindActiveInstanceLock(playerGuid, entries, true, false)) { InstanceResetTimePoint oldExpiryTime = instanceLock->GetEffectiveExpiryTime(); instanceLock->SetExtended(extended); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHARACTER_INSTANCE_LOCK_EXTENSION); stmt->setUInt8(0, extended ? 1 : 0); stmt->setUInt64(1, playerGuid.GetCounter()); stmt->setUInt32(2, entries.MapDifficulty->MapID); stmt->setUInt32(3, entries.MapDifficulty->LockID); CharacterDatabase.Execute(stmt); TC_LOG_DEBUG("instance.locks", "[{}-{} | {}-{}] Instance lock for {} is {} extended", entries.Map->ID, entries.Map->MapName[sWorld->GetDefaultDbcLocale()], uint32(entries.MapDifficulty->DifficultyID), sDifficultyStore.AssertEntry(entries.MapDifficulty->DifficultyID)->Name[sWorld->GetDefaultDbcLocale()], playerGuid.ToString(), extended ? "now" : "no longer"); return { oldExpiryTime, instanceLock->GetEffectiveExpiryTime() }; } return { InstanceResetTimePoint::min(), InstanceResetTimePoint::min() }; } void InstanceLockMgr::ResetInstanceLocksForPlayer(ObjectGuid const& playerGuid, Optional mapId, Optional difficulty, std::vector* locksReset, std::vector* locksFailedToReset) { auto playerLocksItr = _instanceLocksByPlayer.find(playerGuid); if (playerLocksItr == _instanceLocksByPlayer.end()) return; for (PlayerLockMap::value_type const& playerLockPair : playerLocksItr->second) { if (playerLockPair.second->IsInUse()) { locksFailedToReset->push_back(playerLockPair.second.get()); continue; } if (mapId && *mapId != playerLockPair.second->GetMapId()) continue; if (difficulty && *difficulty != playerLockPair.second->GetDifficultyId()) continue; if (playerLockPair.second->IsExpired()) continue; locksReset->push_back(playerLockPair.second.get()); } if (!locksReset->empty()) { CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); for (InstanceLock const* instanceLock : *locksReset) { MapDb2Entries entries(instanceLock->GetMapId(), instanceLock->GetDifficultyId()); InstanceResetTimePoint newExpiryTime = GetNextResetTime(entries) - Seconds(entries.MapDifficulty->GetRaidDuration()); // set reset time to last reset time const_cast(instanceLock)->SetExpiryTime(newExpiryTime); const_cast(instanceLock)->SetExtended(false); CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHARACTER_INSTANCE_LOCK_FORCE_EXPIRE); stmt->setUInt64(0, uint64(std::chrono::system_clock::to_time_t(newExpiryTime))); stmt->setUInt64(1, playerGuid.GetCounter()); stmt->setUInt32(2, entries.MapDifficulty->MapID); stmt->setUInt32(3, entries.MapDifficulty->LockID); trans->Append(stmt); } CharacterDatabase.CommitTransaction(trans); } } InstanceLocksStatistics InstanceLockMgr::GetStatistics() const { InstanceLocksStatistics statistics; statistics.InstanceCount = _instanceLockDataById.size(); statistics.PlayerCount = _instanceLocksByPlayer.size(); return statistics; } InstanceResetTimePoint InstanceLockMgr::GetNextResetTime(MapDb2Entries const& entries) { tm dateTime = *GameTime::GetDateAndTime(); dateTime.tm_sec = 0; dateTime.tm_min = 0; int32 resetHour = sWorld->getIntConfig(CONFIG_RESET_SCHEDULE_HOUR); switch (entries.MapDifficulty->ResetInterval) { case MAP_DIFFICULTY_RESET_DAILY: { if (dateTime.tm_hour >= resetHour) ++dateTime.tm_mday; dateTime.tm_hour = resetHour; break; } case MAP_DIFFICULTY_RESET_WEEKLY: { int32 resetDay = sWorld->getIntConfig(CONFIG_RESET_SCHEDULE_WEEK_DAY); int32 daysAdjust = resetDay - dateTime.tm_wday; if (dateTime.tm_wday > resetDay || (dateTime.tm_wday == resetDay && dateTime.tm_hour >= resetHour)) daysAdjust += 7; // passed it for current week, grab time from next week dateTime.tm_hour = resetHour; dateTime.tm_mday += daysAdjust; break; } default: break; } return std::chrono::system_clock::from_time_t(mktime(&dateTime)); } InstanceLockMgr& InstanceLockMgr::Instance() { static InstanceLockMgr instance; return instance; }