diff --git a/src/common/Utilities/TaskScheduler.cpp b/src/common/Utilities/TaskScheduler.cpp new file mode 100644 index 00000000000000..7a443cbe6d974f --- /dev/null +++ b/src/common/Utilities/TaskScheduler.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + * Copyright (C) 2008+ TrinityCore + */ + +#include "TaskScheduler.h" +#include "Errors.h" + +TaskScheduler& TaskScheduler::ClearValidator() +{ + _predicate = EmptyValidator; + return *this; +} + +TaskScheduler& TaskScheduler::Update(success_t const& callback) +{ + _now = clock_t::now(); + Dispatch(callback); + return *this; +} + +TaskScheduler& TaskScheduler::Update(size_t const milliseconds, success_t const& callback) +{ + return Update(std::chrono::milliseconds(milliseconds), callback); +} + +TaskScheduler& TaskScheduler::Async(std::function const& callable) +{ + _asyncHolder.push(callable); + return *this; +} + +TaskScheduler& TaskScheduler::CancelAll() +{ + /// Clear the task holder + _task_holder.Clear(); + _asyncHolder = AsyncHolder(); + return *this; +} + +TaskScheduler& TaskScheduler::CancelGroup(group_t const group) +{ + _task_holder.RemoveIf([group](TaskContainer const & task) -> bool + { + return task->IsInGroup(group); + }); + return *this; +} + +TaskScheduler& TaskScheduler::CancelGroupsOf(std::vector const& groups) +{ + std::for_each(groups.begin(), groups.end(), + std::bind(&TaskScheduler::CancelGroup, this, std::placeholders::_1)); + + return *this; +} + +TaskScheduler& TaskScheduler::InsertTask(TaskContainer task) +{ + _task_holder.Push(std::move(task)); + return *this; +} + +void TaskScheduler::Dispatch(success_t const& callback) +{ + // If the validation failed abort the dispatching here. + if (!_predicate()) + return; + + // Process all asyncs + while (!_asyncHolder.empty()) + { + _asyncHolder.front()(); + _asyncHolder.pop(); + + // If the validation failed abort the dispatching here. + if (!_predicate()) + return; + } + + while (!_task_holder.IsEmpty()) + { + if (_task_holder.First()->_end > _now) + break; + + // Perfect forward the context to the handler + // Use weak references to catch destruction before callbacks. + TaskContext context(_task_holder.Pop(), std::weak_ptr(self_reference)); + + // Invoke the context + context.Invoke(); + + // If the validation failed abort the dispatching here. + if (!_predicate()) + return; + } + + // On finish call the final callback + callback(); +} + +void TaskScheduler::TaskQueue::Push(TaskContainer&& task) +{ + container.insert(task); +} + +auto TaskScheduler::TaskQueue::Pop() -> TaskContainer +{ + TaskContainer result = *container.begin(); + container.erase(container.begin()); + return result; +} + +auto TaskScheduler::TaskQueue::First() const -> TaskContainer const& +{ + return *container.begin(); +} + +void TaskScheduler::TaskQueue::Clear() +{ + container.clear(); +} + +void TaskScheduler::TaskQueue::RemoveIf(std::function const& filter) +{ + for (auto itr = container.begin(); itr != container.end();) + if (filter(*itr)) + itr = container.erase(itr); + else + ++itr; +} + +void TaskScheduler::TaskQueue::ModifyIf(std::function const& filter) +{ + std::vector cache; + for (auto itr = container.begin(); itr != container.end();) + if (filter(*itr)) + { + cache.push_back(*itr); + itr = container.erase(itr); + } + else + ++itr; + + container.insert(cache.begin(), cache.end()); +} + +bool TaskScheduler::TaskQueue::IsEmpty() const +{ + return container.empty(); +} + +TaskContext& TaskContext::Dispatch(std::function const& apply) +{ + if (auto const owner = _owner.lock()) + apply(*owner); + + return *this; +} + +bool TaskContext::IsExpired() const +{ + return _owner.expired(); +} + +bool TaskContext::IsInGroup(TaskScheduler::group_t const group) const +{ + return _task->IsInGroup(group); +} + +TaskContext& TaskContext::SetGroup(TaskScheduler::group_t const group) +{ + _task->_group = group; + return *this; +} + +TaskContext& TaskContext::ClearGroup() +{ + _task->_group = std::nullopt; + return *this; +} + +TaskScheduler::repeated_t TaskContext::GetRepeatCounter() const +{ + return _task->_repeated; +} + +TaskContext& TaskContext::Async(std::function const& callable) +{ + return Dispatch(std::bind(&TaskScheduler::Async, std::placeholders::_1, callable)); +} + +TaskContext& TaskContext::CancelAll() +{ + return Dispatch(std::mem_fn(&TaskScheduler::CancelAll)); +} + +TaskContext& TaskContext::CancelGroup(TaskScheduler::group_t const group) +{ + return Dispatch(std::bind(&TaskScheduler::CancelGroup, std::placeholders::_1, group)); +} + +TaskContext& TaskContext::CancelGroupsOf(std::vector const& groups) +{ + return Dispatch(std::bind(&TaskScheduler::CancelGroupsOf, std::placeholders::_1, std::cref(groups))); +} + +void TaskContext::AssertOnConsumed() const +{ + // This was adapted to TC to prevent static analysis tools from complaining. + // If you encounter this assertion check if you repeat a TaskContext more then 1 time! + ASSERT(!(*_consumed) && "Bad task logic, task context was consumed already!"); +} + +void TaskContext::Invoke() +{ + _task->_task(*this); +} diff --git a/src/common/Utilities/TaskScheduler.h b/src/common/Utilities/TaskScheduler.h new file mode 100644 index 00000000000000..6c8d8a60465236 --- /dev/null +++ b/src/common/Utilities/TaskScheduler.h @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + * Copyright (C) 2008+ TrinityCore + */ + +#ifndef _TASK_SCHEDULER_H_ +#define _TASK_SCHEDULER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Util.h" + +class TaskContext; + +/// The TaskScheduler class provides the ability to schedule std::function's in the near future. +/// Use TaskScheduler::Update to update the scheduler. +/// Popular methods are: +/// * Schedule (Schedules a std::function which will be executed in the near future). +/// * Schedules an asynchronous function which will be executed at the next update tick. +/// * Cancel, Delay & Reschedule (Methods to manipulate already scheduled tasks). +/// Tasks are organized in groups (uint), multiple tasks can have the same group id, +/// you can provide a group or not, but keep in mind that you can only manipulate specific tasks through its group id! +/// Tasks callbacks use the function signature void(TaskContext) where TaskContext provides +/// access to the function schedule plan which makes it possible to repeat the task +/// with the same duration or a new one. +/// It also provides access to the repeat counter which is useful for task that repeat itself often +/// but behave different every time (spoken event dialogs for example). +class TaskScheduler +{ + friend class TaskContext; + + // Time definitions (use steady clock) + typedef std::chrono::steady_clock clock_t; + typedef clock_t::time_point timepoint_t; + typedef clock_t::duration duration_t; + + // Task group type + typedef uint32 group_t; + // Task repeated type + typedef uint32 repeated_t; + // Task handle type + typedef std::function task_handler_t; + // Predicate type + typedef std::function predicate_t; + // Success handle type + typedef std::function success_t; + + class Task + { + friend class TaskContext; + friend class TaskScheduler; + + timepoint_t _end; + duration_t _duration; + std::optional _group; + repeated_t _repeated; + task_handler_t _task; + + public: + // All Argument construct + Task(timepoint_t const& end, duration_t const& duration, std::optional const& group, + repeated_t const repeated, task_handler_t const& task) + : _end(end), _duration(duration), _group(group), _repeated(repeated), _task(task) { } + + // Minimal Argument construct + Task(timepoint_t const& end, duration_t const& duration, task_handler_t const& task) + : _end(end), _duration(duration), _group(std::nullopt), _repeated(0), _task(task) { } + + // Copy construct + Task(Task const&) = delete; + // Move construct + Task(Task&&) = delete; + // Copy Assign + Task& operator= (Task const&) = default; + // Move Assign + Task& operator= (Task&& right) = delete; + + // Order tasks by its end + inline bool operator< (Task const& other) const + { + return _end < other._end; + } + + inline bool operator> (Task const& other) const + { + return _end > other._end; + } + + // Compare tasks with its end + inline bool operator== (Task const& other) + { + return _end == other._end; + } + + // Returns true if the task is in the given group + inline bool IsInGroup(group_t const group) const + { + return _group == group; + } + }; + + typedef std::shared_ptr TaskContainer; + + /// Container which provides Task order, insert and reschedule operations. + struct Compare + { + bool operator() (TaskContainer const& left, TaskContainer const& right) const + { + return (*left.get()) < (*right.get()); + }; + }; + + class TaskQueue + { + std::multiset container; + + public: + // Pushes the task in the container + void Push(TaskContainer&& task); + + /// Pops the task out of the container + TaskContainer Pop(); + + TaskContainer const& First() const; + + void Clear(); + + void RemoveIf(std::function const& filter); + + void ModifyIf(std::function const& filter); + + bool IsEmpty() const; + }; + + /// Contains a self reference to track if this object was deleted or not. + std::shared_ptr self_reference; + + /// The current time point (now) + timepoint_t _now; + + /// The Task Queue which contains all task objects. + TaskQueue _task_holder; + + typedef std::queue> AsyncHolder; + + /// Contains all asynchronous tasks which will be invoked at + /// the next update tick. + AsyncHolder _asyncHolder; + + predicate_t _predicate; + + static bool EmptyValidator() + { + return true; + } + + static void EmptyCallback() + { + } + +public: + TaskScheduler() + : self_reference(this, [](TaskScheduler const*) { }), _now(clock_t::now()), _predicate(EmptyValidator) { } + + template TaskScheduler(P&& predicate) + : self_reference(this, [](TaskScheduler const*) { }), _now(clock_t::now()), _predicate(std::forward

(predicate)) { } + + TaskScheduler(TaskScheduler const&) = delete; + TaskScheduler(TaskScheduler&&) = delete; + TaskScheduler& operator= (TaskScheduler const&) = delete; + TaskScheduler& operator= (TaskScheduler&&) = delete; + + /// Sets a validator which is asked if tasks are allowed to be executed. + template + TaskScheduler& SetValidator(P&& predicate) + { + _predicate = std::forward

(predicate); + return *this; + } + + /// Clears the validator which is asked if tasks are allowed to be executed. + TaskScheduler& ClearValidator(); + + /// Update the scheduler to the current time. + /// Calls the optional callback on successfully finish. + TaskScheduler& Update(success_t const& callback = EmptyCallback); + + /// Update the scheduler with a difftime in ms. + /// Calls the optional callback on successfully finish. + TaskScheduler& Update(size_t const milliseconds, success_t const& callback = EmptyCallback); + + /// Update the scheduler with a difftime. + /// Calls the optional callback on successfully finish. + template + TaskScheduler& Update(std::chrono::duration<_Rep, _Period> const& difftime, + success_t const& callback = EmptyCallback) + { + _now += difftime; + Dispatch(callback); + return *this; + } + + /// Schedule an callable function that is executed at the next update tick. + /// Its safe to modify the TaskScheduler from within the callable. + TaskScheduler& Async(std::function const& callable); + + /// Schedule an event with a fixed rate. + /// Never call this from within a task context! Use TaskContext::Schedule instead! + template + TaskScheduler& Schedule(std::chrono::duration<_Rep, _Period> const& time, + task_handler_t const& task) + { + return ScheduleAt(_now, time, task); + } + + /// Schedule an event with a fixed rate. + /// Never call this from within a task context! Use TaskContext::Schedule instead! + template + TaskScheduler& Schedule(std::chrono::duration<_Rep, _Period> const& time, + group_t const group, task_handler_t const& task) + { + return ScheduleAt(_now, time, group, task); + } + + /// Schedule an event with a randomized rate between min and max rate. + /// Never call this from within a task context! Use TaskContext::Schedule instead! + template + TaskScheduler& Schedule(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max, task_handler_t const& task) + { + return Schedule(RandomDurationBetween(min, max), task); + } + + /// Schedule an event with a fixed rate. + /// Never call this from within a task context! Use TaskContext::Schedule instead! + template + TaskScheduler& Schedule(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max, group_t const group, + task_handler_t const& task) + { + return Schedule(RandomDurationBetween(min, max), group, task); + } + + /// Cancels all tasks. + /// Never call this from within a task context! Use TaskContext::CancelAll instead! + TaskScheduler& CancelAll(); + + /// Cancel all tasks of a single group. + /// Never call this from within a task context! Use TaskContext::CancelGroup instead! + TaskScheduler& CancelGroup(group_t const group); + + /// Cancels all groups in the given std::vector. + /// Hint: Use std::initializer_list for this: "{1, 2, 3, 4}" + TaskScheduler& CancelGroupsOf(std::vector const& groups); + + /// Delays all tasks with the given duration. + template + TaskScheduler& DelayAll(std::chrono::duration<_Rep, _Period> const& duration) + { + _task_holder.ModifyIf([&duration](TaskContainer const & task) -> bool + { + task->_end += duration; + return true; + }); + return *this; + } + + /// Delays all tasks with a random duration between min and max. + template + TaskScheduler& DelayAll(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return DelayAll(RandomDurationBetween(min, max)); + } + + /// Delays all tasks of a group with the given duration. + template + TaskScheduler& DelayGroup(group_t const group, std::chrono::duration<_Rep, _Period> const& duration) + { + _task_holder.ModifyIf([&duration, group](TaskContainer const & task) -> bool + { + if (task->IsInGroup(group)) + { + task->_end += duration; + return true; + } + else + return false; + }); + return *this; + } + + /// Delays all tasks of a group with a random duration between min and max. + template + TaskScheduler& DelayGroup(group_t const group, + std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return DelayGroup(group, RandomDurationBetween(min, max)); + } + + /// Reschedule all tasks with a given duration. + template + TaskScheduler& RescheduleAll(std::chrono::duration<_Rep, _Period> const& duration) + { + auto const end = _now + duration; + _task_holder.ModifyIf([end](TaskContainer const & task) -> bool + { + task->_end = end; + return true; + }); + return *this; + } + + /// Reschedule all tasks with a random duration between min and max. + template + TaskScheduler& RescheduleAll(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return RescheduleAll(RandomDurationBetween(min, max)); + } + + /// Reschedule all tasks of a group with the given duration. + template + TaskScheduler& RescheduleGroup(group_t const group, std::chrono::duration<_Rep, _Period> const& duration) + { + auto const end = _now + duration; + _task_holder.ModifyIf([end, group](TaskContainer const & task) -> bool + { + if (task->IsInGroup(group)) + { + task->_end = end; + return true; + } + else + return false; + }); + return *this; + } + + /// Reschedule all tasks of a group with a random duration between min and max. + template + TaskScheduler& RescheduleGroup(group_t const group, + std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return RescheduleGroup(group, RandomDurationBetween(min, max)); + } + +private: + /// Insert a new task to the enqueued tasks. + TaskScheduler& InsertTask(TaskContainer task); + + template + TaskScheduler& ScheduleAt(timepoint_t const& end, + std::chrono::duration<_Rep, _Period> const& time, task_handler_t const& task) + { + return InsertTask(TaskContainer(new Task(end + time, time, task))); + } + + /// Schedule an event with a fixed rate. + /// Never call this from within a task context! Use TaskContext::schedule instead! + template + TaskScheduler& ScheduleAt(timepoint_t const& end, + std::chrono::duration<_Rep, _Period> const& time, + group_t const group, task_handler_t const& task) + { + static repeated_t const DEFAULT_REPEATED = 0; + return InsertTask(TaskContainer(new Task(end + time, time, group, DEFAULT_REPEATED, task))); + } + + // Returns a random duration between min and max + template + static std::chrono::milliseconds RandomDurationBetween(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + auto const milli_min = std::chrono::duration_cast(min); + auto const milli_max = std::chrono::duration_cast(max); + + // TC specific: use SFMT URandom + return std::chrono::milliseconds(urand(uint32(milli_min.count()), uint32(milli_max.count()))); + } + + /// Dispatch remaining tasks + void Dispatch(success_t const& callback); +}; + +class TaskContext +{ + friend class TaskScheduler; + + /// Associated task + TaskScheduler::TaskContainer _task; + + /// Owner + std::weak_ptr _owner; + + /// Marks the task as consumed + std::shared_ptr _consumed; + + /// Dispatches an action safe on the TaskScheduler + TaskContext& Dispatch(std::function const& apply); + +public: + // Empty constructor + TaskContext() + : _task(), _owner(), _consumed(std::make_shared(true)) { } + + // Construct from task and owner + explicit TaskContext(TaskScheduler::TaskContainer&& task, std::weak_ptr&& owner) + : _task(task), _owner(owner), _consumed(std::make_shared(false)) { } + + // Copy construct + TaskContext(TaskContext const& right) + : _task(right._task), _owner(right._owner), _consumed(right._consumed) { } + + // Move construct + TaskContext(TaskContext&& right) + : _task(std::move(right._task)), _owner(std::move(right._owner)), _consumed(std::move(right._consumed)) { } + + // Copy assign + TaskContext& operator= (TaskContext const& right) + { + _task = right._task; + _owner = right._owner; + _consumed = right._consumed; + return *this; + } + + // Move assign + TaskContext& operator= (TaskContext&& right) + { + _task = std::move(right._task); + _owner = std::move(right._owner); + _consumed = std::move(right._consumed); + return *this; + } + + /// Returns true if the owner was deallocated and this context has expired. + bool IsExpired() const; + + /// Returns true if the event is in the given group + bool IsInGroup(TaskScheduler::group_t const group) const; + + /// Sets the event in the given group + TaskContext& SetGroup(TaskScheduler::group_t const group); + + /// Removes the group from the event + TaskContext& ClearGroup(); + + /// Returns the repeat counter which increases every time the task is repeated. + TaskScheduler::repeated_t GetRepeatCounter() const; + + /// Repeats the event and sets a new duration. + /// std::chrono::seconds(5) for example. + /// This will consume the task context, its not possible to repeat the task again + /// from the same task context! + template + TaskContext& Repeat(std::chrono::duration<_Rep, _Period> const& duration) + { + AssertOnConsumed(); + + // Set new duration, in-context timing and increment repeat counter + _task->_duration = duration; + _task->_end += duration; + _task->_repeated += 1; + (*_consumed) = true; + return Dispatch(std::bind(&TaskScheduler::InsertTask, std::placeholders::_1, _task)); + } + + /// Repeats the event with the same duration. + /// This will consume the task context, its not possible to repeat the task again + /// from the same task context! + TaskContext& Repeat() + { + return Repeat(_task->_duration); + } + + /// Repeats the event and set a new duration that is randomized between min and max. + /// std::chrono::seconds(5) for example. + /// This will consume the task context, its not possible to repeat the task again + /// from the same task context! + template + TaskContext& Repeat(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return Repeat(TaskScheduler::RandomDurationBetween(min, max)); + } + + /// Schedule a callable function that is executed at the next update tick from within the context. + /// Its safe to modify the TaskScheduler from within the callable. + TaskContext& Async(std::function const& callable); + + /// Schedule an event with a fixed rate from within the context. + /// Its possible that the new event is executed immediately! + /// Use TaskScheduler::Async to create a task + /// which will be called at the next update tick. + template + TaskContext& Schedule(std::chrono::duration<_Rep, _Period> const& time, + TaskScheduler::task_handler_t const& task) + { + auto const end = _task->_end; + return Dispatch([end, time, task](TaskScheduler & scheduler) -> TaskScheduler & + { + return scheduler.ScheduleAt<_Rep, _Period>(end, time, task); + }); + } + + /// Schedule an event with a fixed rate from within the context. + /// Its possible that the new event is executed immediately! + /// Use TaskScheduler::Async to create a task + /// which will be called at the next update tick. + template + TaskContext& Schedule(std::chrono::duration<_Rep, _Period> const& time, + TaskScheduler::group_t const group, TaskScheduler::task_handler_t const& task) + { + auto const end = _task->_end; + return Dispatch([end, time, group, task](TaskScheduler & scheduler) -> TaskScheduler & + { + return scheduler.ScheduleAt<_Rep, _Period>(end, time, group, task); + }); + } + + /// Schedule an event with a randomized rate between min and max rate from within the context. + /// Its possible that the new event is executed immediately! + /// Use TaskScheduler::Async to create a task + /// which will be called at the next update tick. + template + TaskContext& Schedule(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max, TaskScheduler::task_handler_t const& task) + { + return Schedule(TaskScheduler::RandomDurationBetween(min, max), task); + } + + /// Schedule an event with a randomized rate between min and max rate from within the context. + /// Its possible that the new event is executed immediately! + /// Use TaskScheduler::Async to create a task + /// which will be called at the next update tick. + template + TaskContext& Schedule(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max, TaskScheduler::group_t const group, + TaskScheduler::task_handler_t const& task) + { + return Schedule(TaskScheduler::RandomDurationBetween(min, max), group, task); + } + + /// Cancels all tasks from within the context. + TaskContext& CancelAll(); + + /// Cancel all tasks of a single group from within the context. + TaskContext& CancelGroup(TaskScheduler::group_t const group); + + /// Cancels all groups in the given std::vector from within the context. + /// Hint: Use std::initializer_list for this: "{1, 2, 3, 4}" + TaskContext& CancelGroupsOf(std::vector const& groups); + + /// Delays all tasks with the given duration from within the context. + template + TaskContext& DelayAll(std::chrono::duration<_Rep, _Period> const& duration) + { + return Dispatch(std::bind(&TaskScheduler::DelayAll<_Rep, _Period>, std::placeholders::_1, duration)); + } + + /// Delays all tasks with a random duration between min and max from within the context. + template + TaskContext& DelayAll(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return DelayAll(TaskScheduler::RandomDurationBetween(min, max)); + } + + /// Delays all tasks of a group with the given duration from within the context. + template + TaskContext& DelayGroup(TaskScheduler::group_t const group, std::chrono::duration<_Rep, _Period> const& duration) + { + return Dispatch(std::bind(&TaskScheduler::DelayGroup<_Rep, _Period>, std::placeholders::_1, group, duration)); + } + + /// Delays all tasks of a group with a random duration between min and max from within the context. + template + TaskContext& DelayGroup(TaskScheduler::group_t const group, + std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return DelayGroup(group, TaskScheduler::RandomDurationBetween(min, max)); + } + + /// Reschedule all tasks with the given duration. + template + TaskContext& RescheduleAll(std::chrono::duration<_Rep, _Period> const& duration) + { + return Dispatch(std::bind(&TaskScheduler::RescheduleAll, std::placeholders::_1, duration)); + } + + /// Reschedule all tasks with a random duration between min and max. + template + TaskContext& RescheduleAll(std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return RescheduleAll(TaskScheduler::RandomDurationBetween(min, max)); + } + + /// Reschedule all tasks of a group with the given duration. + template + TaskContext& RescheduleGroup(TaskScheduler::group_t const group, std::chrono::duration<_Rep, _Period> const& duration) + { + return Dispatch(std::bind(&TaskScheduler::RescheduleGroup<_Rep, _Period>, std::placeholders::_1, group, duration)); + } + + /// Reschedule all tasks of a group with a random duration between min and max. + template + TaskContext& RescheduleGroup(TaskScheduler::group_t const group, + std::chrono::duration<_RepLeft, _PeriodLeft> const& min, + std::chrono::duration<_RepRight, _PeriodRight> const& max) + { + return RescheduleGroup(group, TaskScheduler::RandomDurationBetween(min, max)); + } + +private: + /// Asserts if the task was consumed already. + void AssertOnConsumed() const; + + /// Invokes the associated hook of the task. + void Invoke(); +}; + +#endif diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index b5ecfa300df813..1bac195fc6a136 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -77,6 +77,7 @@ #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include "QuestTracker.h" #ifdef ELUNA #include "LuaEngine.h" @@ -15896,17 +15897,10 @@ void Player::AddQuest(Quest const* quest, Object* questGiver) SendQuestUpdate(quest_id); // check if Quest Tracker is enabled - if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) { - // prepare Quest Tracker datas - auto stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_QUEST_TRACK); - stmt->setUInt32(0, quest_id); - stmt->setUInt32(1, GetGUIDLow()); - stmt->setString(2, _HASH); - stmt->setString(3, _DATE); - // add to Quest Tracker - CharacterDatabase.Execute(stmt); + sQuestTracker->Add(quest_id, GetGUIDLow(), GitRevision::GetHash(), GitRevision::GetDate()); } // Xinef: area auras may change on quest accept! @@ -15941,15 +15935,10 @@ void Player::CompleteQuest(uint32 quest_id) AdditionalSavingAddMask(ADDITIONAL_SAVING_INVENTORY_AND_GOLD | ADDITIONAL_SAVING_QUEST_STATUS); // check if Quest Tracker is enabled - if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) { - // prepare Quest Tracker datas - auto stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_COMPLETE_TIME); - stmt->setUInt32(0, quest_id); - stmt->setUInt32(1, GetGUIDLow()); - // add to Quest Tracker - CharacterDatabase.Execute(stmt); + sQuestTracker->UpdateCompleteTime(quest_id, GetGUIDLow()); } } diff --git a/src/server/game/Handlers/QuestHandler.cpp b/src/server/game/Handlers/QuestHandler.cpp index ec3cd8b403b0f1..f35d3e8800f182 100644 --- a/src/server/game/Handlers/QuestHandler.cpp +++ b/src/server/game/Handlers/QuestHandler.cpp @@ -21,6 +21,7 @@ #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include "QuestTracker.h" #ifdef ELUNA #include "LuaEngine.h" @@ -430,15 +431,10 @@ void WorldSession::HandleQuestLogRemoveQuest(WorldPacket& recvData) sLog->outDetail("Player %u abandoned quest %u", _player->GetGUIDLow(), questId); #endif // check if Quest Tracker is enabled - if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) { - // prepare Quest Tracker datas - auto stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_ABANDON_TIME); - stmt->setUInt32(0, questId); - stmt->setUInt32(1, _player->GetGUIDLow()); - // add to Quest Tracker - CharacterDatabase.Execute(stmt); + sQuestTracker->UpdateAbandonTime(questId, _player->GetGUIDLow()); } } diff --git a/src/server/game/Quests/QuestTracker.cpp b/src/server/game/Quests/QuestTracker.cpp new file mode 100644 index 00000000000000..7160f6c1993f76 --- /dev/null +++ b/src/server/game/Quests/QuestTracker.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + * Copyright (C) 2021 WarheadCore + */ + +#include "QuestTracker.h" +#include "DatabaseEnv.h" +#include "Duration.h" +#include "World.h" +#include "TaskScheduler.h" +#include "StringFormat.h" +#include +#include + +namespace +{ + // Typdefs + using QuestTrackInsert = std::vector>; + using QuestTrackUpdate = std::vector>; + + QuestTrackInsert _questTrackStore; + QuestTrackUpdate _questCompleteStore; + QuestTrackUpdate _questAbandonStore; + QuestTrackUpdate _questGMCompleteStore; + + // Scheduler - for update queue + TaskScheduler scheduler; +} + +QuestTracker* QuestTracker::instance() +{ + static QuestTracker instance; + return &instance; +} + +void QuestTracker::InitSystem() +{ + if (!sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) + { + sLog->outString(">> The QuestTracker System is disabled"); + return; + } + + SetExecuteDelay(); + + sLog->outString(">> System loading"); +} + +void QuestTracker::Update(uint32 diff) +{ + if (!sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) + return; + + scheduler.Update(diff); +} + +void QuestTracker::SetExecuteDelay() +{ + if (!sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE) || !sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_QUEUE_ENABLE)) + return; + + Seconds updateSecs = Seconds(sWorld->getIntConfig(CONFIG_QUEST_TRACKER_QUEUE_DELAY)); + if (updateSecs < 1s) + { + sLog->outError("> QuestTracker: ExecuteDelay < 1 second. Set 10 seconds"); + updateSecs = 10s; + return; + } + + scheduler.CancelAll(); + + scheduler.Schedule(updateSecs, [this](TaskContext context) + { + Execute(); + + context.Repeat(); + }); +} + +void QuestTracker::Execute() +{ + if (_questTrackStore.empty() && + _questCompleteStore.empty() && + _questAbandonStore.empty() && + _questGMCompleteStore.empty()) + return; + + sLog->outString("> QuestTracker: Start Execute..."); + + uint32 msTimeStart = getMSTime(); + + /// Insert section + if (!_questTrackStore.empty()) + { + for (auto const& [ID, CharacterLowGuid, Hash, Revision] : _questTrackStore) + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_QUEST_TRACK); + stmt->setUInt32(0, ID); + stmt->setUInt32(1, CharacterLowGuid); + stmt->setString(2, Hash); + stmt->setString(3, Revision); + CharacterDatabase.Execute(stmt); + } + + sLog->outString("> QuestTracker: Execute 'CHAR_INS_QUEST_TRACK' (%u)", static_cast(_questTrackStore.size())); + + _questTrackStore.clear(); + } + + /// Update section + auto SendUpdate = [&](QuestTrackUpdate& updateStore, CharacterDatabaseStatements stmtIndex, std::string const& updateType) + { + if (updateStore.empty()) + return; + + auto SendUpdateQuestTracker = [&](uint32 questID, uint32 characterLowGuid) + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(stmtIndex); + stmt->setUInt32(0, questID); + stmt->setUInt32(1, characterLowGuid); + CharacterDatabase.Execute(stmt); + }; + + for (auto const& [questID, characterLowGuid] : updateStore) + SendUpdateQuestTracker(questID, characterLowGuid); + + sLog->outString("> QuestTracker: Execute '%s' (%u)", updateType.c_str(), static_cast(updateStore.size())); + + updateStore.clear(); + }; + + SendUpdate(_questCompleteStore, CHAR_UPD_QUEST_TRACK_COMPLETE_TIME, "CHAR_UPD_QUEST_TRACK_COMPLETE_TIME"); + SendUpdate(_questAbandonStore, CHAR_UPD_QUEST_TRACK_ABANDON_TIME, "CHAR_UPD_QUEST_TRACK_ABANDON_TIME"); + SendUpdate(_questGMCompleteStore, CHAR_UPD_QUEST_TRACK_GM_COMPLETE, "CHAR_UPD_QUEST_TRACK_GM_COMPLETE"); + + sLog->outString("> QuestTracker: Execute end in %u ms", GetMSTimeDiffToNow(msTimeStart)); +} + +void QuestTracker::Add(uint32 questID, uint32 characterLowGuid, std::string const& coreHash, std::string const& coreRevision) +{ + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_QUEUE_ENABLE)) + _questTrackStore.emplace_back(questID, characterLowGuid, coreHash, coreRevision); + else + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_QUEST_TRACK); + stmt->setUInt32(0, questID); + stmt->setUInt32(1, characterLowGuid); + stmt->setString(2, coreHash); + stmt->setString(3, coreRevision); + CharacterDatabase.Execute(stmt); + } +} + +void QuestTracker::UpdateCompleteTime(uint32 questID, uint32 characterLowGuid) +{ + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_QUEUE_ENABLE)) + _questCompleteStore.emplace_back(questID, characterLowGuid); + else + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_COMPLETE_TIME); + stmt->setUInt32(0, questID); + stmt->setUInt32(1, characterLowGuid); + CharacterDatabase.Execute(stmt); + } +} + +void QuestTracker::UpdateAbandonTime(uint32 questID, uint32 characterLowGuid) +{ + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_QUEUE_ENABLE)) + _questAbandonStore.emplace_back(questID, characterLowGuid); + else + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_ABANDON_TIME); + stmt->setUInt32(0, questID); + stmt->setUInt32(1, characterLowGuid); + CharacterDatabase.Execute(stmt); + } +} + +void QuestTracker::UpdateGMComplete(uint32 questID, uint32 characterLowGuid) +{ + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_QUEUE_ENABLE)) + _questGMCompleteStore.emplace_back(questID, characterLowGuid); + else + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_GM_COMPLETE); + stmt->setUInt32(0, questID); + stmt->setUInt32(1, characterLowGuid); + CharacterDatabase.Execute(stmt); + } +} diff --git a/src/server/game/Quests/QuestTracker.h b/src/server/game/Quests/QuestTracker.h new file mode 100644 index 00000000000000..a2cf8ce6da6e3b --- /dev/null +++ b/src/server/game/Quests/QuestTracker.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + * Copyright (C) 2020+ WarheadCore + */ + +#ifndef _QUEST_TRACKER_H +#define _QUEST_TRACKER_H + +#include "Common.h" + +class QuestTracker +{ +public: + static QuestTracker* instance(); + + void InitSystem(); + void Update(uint32 diff); + void SetExecuteDelay(); + void Execute(); + + void Add(uint32 questID, uint32 characterLowGuid, std::string const& coreHash, std::string const& coreRevision); + void UpdateCompleteTime(uint32 questID, uint32 characterLowGuid); + void UpdateAbandonTime(uint32 questID, uint32 characterLowGuid); + void UpdateGMComplete(uint32 questID, uint32 characterLowGuid); +}; + +#define sQuestTracker QuestTracker::instance() + +#endif diff --git a/src/server/game/World/IWorld.h b/src/server/game/World/IWorld.h index bf0aa6bf945ca1..30aa28e346ef3e 100644 --- a/src/server/game/World/IWorld.h +++ b/src/server/game/World/IWorld.h @@ -145,7 +145,6 @@ enum WorldBoolConfigs CONFIG_DONT_CACHE_RANDOM_MOVEMENT_PATHS, // pussywizard CONFIG_QUEST_IGNORE_AUTO_ACCEPT, CONFIG_QUEST_IGNORE_AUTO_COMPLETE, - CONFIG_QUEST_ENABLE_QUEST_TRACKER, CONFIG_WARDEN_ENABLED, CONFIG_ENABLE_CONTINENT_TRANSPORT, CONFIG_ENABLE_CONTINENT_TRANSPORT_PRELOADING, @@ -164,6 +163,8 @@ enum WorldBoolConfigs CONFIG_DEBUG_BATTLEGROUND, CONFIG_DEBUG_ARENA, CONFIG_REGEN_HP_CANNOT_REACH_TARGET_IN_RAID, + CONFIG_QUEST_TRACKER_ENABLE, + CONFIG_QUEST_TRACKER_QUEUE_ENABLE, BOOL_CONFIG_VALUE_COUNT }; @@ -373,6 +374,7 @@ enum WorldIntConfigs CONFIG_TOGGLE_XP_COST, CONFIG_NPC_EVADE_IF_NOT_REACHABLE, CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID, + CONFIG_QUEST_TRACKER_QUEUE_DELAY, INT_CONFIG_VALUE_COUNT }; diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index f8692b9ad7385e..7e0223152a1624 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -76,6 +76,7 @@ #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include "QuestTracker.h" #include #ifdef ELUNA @@ -1041,8 +1042,6 @@ void World::LoadConfigSettings(bool reload) m_int_configs[CONFIG_WORLD_BOSS_LEVEL_DIFF] = sConfigMgr->GetOption("WorldBossLevelDiff", 3); - m_bool_configs[CONFIG_QUEST_ENABLE_QUEST_TRACKER] = sConfigMgr->GetOption("Quests.EnableQuestTracker", false); - // note: disable value (-1) will assigned as 0xFFFFFFF, to prevent overflow at calculations limit it to max possible player level MAX_LEVEL(100) m_int_configs[CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF] = sConfigMgr->GetOption("Quests.LowLevelHideDiff", 4); if (m_int_configs[CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF] > MAX_LEVEL) @@ -1413,6 +1412,11 @@ void World::LoadConfigSettings(bool reload) m_int_configs[CONFIG_GM_LEVEL_CHANNEL_MODERATION] = sConfigMgr->GetOption("Channel.ModerationGMLevel", 1); + // Quest Tracker + m_bool_configs[CONFIG_QUEST_TRACKER_ENABLE] = sConfigMgr->GetOption("QuestTracker.Enable", false); + m_bool_configs[CONFIG_QUEST_TRACKER_QUEUE_ENABLE] = sConfigMgr->GetOption("QuestTracker.Queue.Enable", false); + m_int_configs[CONFIG_QUEST_TRACKER_QUEUE_DELAY] = sConfigMgr->GetOption("QuestTracker.Queue.Delay", 10); + // call ScriptMgr if we're reloading the configuration sScriptMgr->OnAfterConfigLoad(reload); } @@ -2048,6 +2052,9 @@ void World::SetInitialWorldSettings() mgr = ChannelMgr::forTeam(TEAM_HORDE); mgr->LoadChannels(); + sLog->outString("Initialising QuestTracker system..."); + sQuestTracker->InitSystem(); + #ifdef ELUNA ///- Run eluna scripts. // in multithread foreach: run scripts @@ -2367,6 +2374,8 @@ void World::Update(uint32 diff) sScriptMgr->OnWorldUpdate(diff); SavingSystemMgr::Update(diff); + + sQuestTracker->Update(diff); } void World::ForceGameEventUpdate() diff --git a/src/server/scripts/Commands/cs_quest.cpp b/src/server/scripts/Commands/cs_quest.cpp index e32ec1a94815e3..d9477a41921c56 100644 --- a/src/server/scripts/Commands/cs_quest.cpp +++ b/src/server/scripts/Commands/cs_quest.cpp @@ -16,6 +16,7 @@ EndScriptData */ #include "Player.h" #include "ReputationMgr.h" #include "ScriptMgr.h" +#include "QuestTracker.h" class quest_commandscript : public CommandScript { @@ -232,15 +233,10 @@ class quest_commandscript : public CommandScript player->ModifyMoney(-ReqOrRewMoney); // check if Quest Tracker is enabled - if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) + if (sWorld->getBoolConfig(CONFIG_QUEST_TRACKER_ENABLE)) { - // prepare Quest Tracker datas - auto stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_GM_COMPLETE); - stmt->setUInt32(0, quest->GetQuestId()); - stmt->setUInt32(1, player->GetGUIDLow()); - // add to Quest Tracker - CharacterDatabase.Execute(stmt); + sQuestTracker->UpdateGMComplete(quest->GetQuestId(), player->GetGUIDLow()); } player->CompleteQuest(entry); diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 8e9a5beba2de83..3f432a06aee10f 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -1303,12 +1303,24 @@ Instance.ResetTimeHour = 4 Instance.UnloadDelay = 1800000 # -# Quests.EnableQuestTracker +# QuestTracker.Enable # Description: Store data in the database about quest completion and abandonment to help finding bugged quests. # Default: 0 - (Disabled) # 1 - (Enabled) +# +# QuestTracker.Queue.Enable +# Description: Enable queue +# Default: 0 - (Disabled) +# 1 - (Enabled) +# +# QuestTracker.Queue.Delay +# Description: Delay for update queue in seconds. 1 seconds minimun +# Default: 10 +# -Quests.EnableQuestTracker = 0 +QuestTracker.Enable = 0 +QuestTracker.Queue.Enable = 0 +QuestTracker.Queue.Delay = 10 # # Quests.LowLevelHideDiff