diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index d0611ae65..bb932490c 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -69,6 +69,7 @@ SET(common_sources rulesys.cpp say_link.cpp serialize_buffer.cpp + server_event_scheduler.cpp serverinfo.cpp shareddb.cpp skills.cpp @@ -252,6 +253,7 @@ SET(repositories repositories/base/base_rule_sets_repository.h repositories/base/base_rule_values_repository.h repositories/base/base_saylink_repository.h + repositories/base/base_server_scheduled_events_repository.h repositories/base/base_skill_caps_repository.h repositories/base/base_spawn2_repository.h repositories/base/base_spawnentry_repository.h @@ -415,6 +417,7 @@ SET(repositories repositories/rule_sets_repository.h repositories/rule_values_repository.h repositories/saylink_repository.h + repositories/server_scheduled_events_repository.h repositories/skill_caps_repository.h repositories/spawn2_repository.h repositories/spawnentry_repository.h @@ -461,6 +464,7 @@ SET(common_headers cli/argh.h cli/eqemu_command_handler.h cli/terminal_color.hpp + cron/croncpp.h database/database_dump_service.h data_verification.h database.h @@ -543,6 +547,7 @@ SET(common_headers say_link.h seperator.h serialize_buffer.h + server_event_scheduler.h serverinfo.h servertalk.h shareddb.h diff --git a/common/content/world_content_service.cpp b/common/content/world_content_service.cpp index 949ebcc0d..feb0f5b76 100644 --- a/common/content/world_content_service.cpp +++ b/common/content/world_content_service.cpp @@ -22,6 +22,7 @@ #include "../database.h" #include "../rulesys.h" #include "../eqemu_logsys.h" +#include "../repositories/content_flags_repository.h" WorldContentService::WorldContentService() @@ -99,3 +100,21 @@ bool WorldContentService::IsContentFlagEnabled(const std::string& content_flag) return false; } + +void WorldContentService::ReloadContentFlags(Database &db) +{ + std::vector set_content_flags; + auto content_flags = ContentFlagsRepository::GetWhere(db, "enabled = 1"); + + set_content_flags.reserve(content_flags.size()); + for (auto &flags: content_flags) { + set_content_flags.push_back(flags.flag_name); + } + + LogInfo( + "Enabled content flags [{}]", + implode(", ", set_content_flags) + ); + + SetContentFlags(set_content_flags); +} diff --git a/common/content/world_content_service.h b/common/content/world_content_service.h index 1edc91160..47220b349 100644 --- a/common/content/world_content_service.h +++ b/common/content/world_content_service.h @@ -24,6 +24,8 @@ #include #include +class Database; + namespace Expansion { static const int EXPANSION_ALL = -1; static const int EXPANSION_FILTER_MAX = 99; @@ -165,6 +167,7 @@ public: const std::vector &GetContentFlags() const; bool IsContentFlagEnabled(const std::string& content_flag); void SetContentFlags(std::vector content_flags); + void ReloadContentFlags(Database &db); void SetExpansionContext(); }; diff --git a/common/cron/croncpp.h b/common/cron/croncpp.h new file mode 100644 index 000000000..5a22a7f72 --- /dev/null +++ b/common/cron/croncpp.h @@ -0,0 +1,876 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus > 201402L +#include +#define CRONCPP_IS_CPP17 +#endif + +namespace cron +{ +#ifdef CRONCPP_IS_CPP17 + #define HAS_STRING_VIEW + #define STRING_VIEW std::string_view + #define STRING_VIEW_NPOS std::string_view::npos + #define CONSTEXPTR constexpr +#else +#define STRING_VIEW std::string const & +#define STRING_VIEW_NPOS std::string::npos +#define CONSTEXPTR +#endif + + using cron_int = uint8_t; + + constexpr std::time_t INVALID_TIME = static_cast(-1); + + constexpr size_t INVALID_CRON_INDEX = static_cast(-1); + + class cronexpr; + + namespace detail + { + enum class cron_field + { + second, + minute, + hour_of_day, + day_of_week, + day_of_month, + month, + year + }; + + template + static bool find_next(cronexpr const & cex, + std::tm& date, + size_t const dot); + } + + struct bad_cronexpr : public std::runtime_error + { + public: + explicit bad_cronexpr(STRING_VIEW message) : + std::runtime_error(message.data()) + {} + }; + + + struct cron_standard_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + static std::vector& DAYS() + { + static std::vector days = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + struct cron_oracle_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 0; + static const cron_int CRON_MAX_MONTHS = 11; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + + static std::vector& DAYS() + { + static std::vector days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + struct cron_quartz_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + static std::vector& DAYS() + { + static std::vector days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + class cronexpr; + + template + static cronexpr make_cron(STRING_VIEW expr); + + class cronexpr + { + std::bitset<60> seconds; + std::bitset<60> minutes; + std::bitset<24> hours; + std::bitset<7> days_of_week; + std::bitset<31> days_of_month; + std::bitset<12> months; + + friend bool operator==(cronexpr const & e1, cronexpr const & e2); + friend bool operator!=(cronexpr const & e1, cronexpr const & e2); + + template + friend bool detail::find_next(cronexpr const & cex, + std::tm& date, + size_t const dot); + + friend std::string to_string(cronexpr const & cex); + + template + friend cronexpr make_cron(STRING_VIEW expr); + }; + + inline bool operator==(cronexpr const & e1, cronexpr const & e2) + { + return + e1.seconds == e2.seconds && + e1.minutes == e2.minutes && + e1.hours == e2.hours && + e1.days_of_week == e2.days_of_week && + e1.days_of_month == e2.days_of_month && + e1.months == e2.months; + } + + inline bool operator!=(cronexpr const & e1, cronexpr const & e2) + { + return !(e1 == e2); + } + + inline std::string to_string(cronexpr const & cex) + { + return + cex.seconds.to_string() + " " + + cex.minutes.to_string() + " " + + cex.hours.to_string() + " " + + cex.days_of_month.to_string() + " " + + cex.months.to_string() + " " + + cex.days_of_week.to_string(); + } + + namespace utils + { + inline std::time_t tm_to_time(std::tm& date) + { + return std::mktime(&date); + } + + inline std::tm* time_to_tm(std::time_t const * date, std::tm* const out) + { +#ifdef _WIN32 + errno_t err = localtime_s(out, date); + return 0 == err ? out : nullptr; +#else + return localtime_r(date, out); +#endif + } + + inline std::tm to_tm(STRING_VIEW time) + { + std::istringstream str(time.data()); + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + + std::tm result; + str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Parsing date failed!"); + + result.tm_isdst = -1; // DST info not available + + return result; + } + + inline std::string to_string(std::tm const & tm) + { + std::ostringstream str; + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Writing date failed!"); + + return str.str(); + } + + inline std::string to_upper(std::string text) + { + std::transform(std::begin(text), std::end(text), + std::begin(text), static_cast(std::toupper)); + + return text; + } + + static std::vector split(STRING_VIEW text, char const delimiter) + { + std::vector tokens; + std::string token; + std::istringstream tokenStream(text.data()); + while (std::getline(tokenStream, token, delimiter)) + { + tokens.push_back(token); + } + return tokens; + } + + CONSTEXPTR inline bool contains(STRING_VIEW text, char const ch) noexcept + { + return STRING_VIEW_NPOS != text.find_first_of(ch); + } + } + + namespace detail + { + + inline cron_int to_cron_int(STRING_VIEW text) + { + try + { + return static_cast(std::stoul(text.data())); + } + catch (std::exception const & ex) + { + throw bad_cronexpr(ex.what()); + } + } + + static std::string replace_ordinals( + std::string text, + std::vector const & replacement) + { + for (size_t i = 0; i < replacement.size(); ++i) + { + auto pos = text.find(replacement[i]); + if (std::string::npos != pos) + text.replace(pos, 3 ,std::to_string(i)); + } + + return text; + } + + static std::pair make_range( + STRING_VIEW field, + cron_int const minval, + cron_int const maxval) + { + cron_int first = 0; + cron_int last = 0; + if (field.size() == 1 && field[0] == '*') + { + first = minval; + last = maxval; + } + else if (!utils::contains(field, '-')) + { + first = to_cron_int(field); + last = first; + } + else + { + auto parts = utils::split(field, '-'); + if (parts.size() != 2) + throw bad_cronexpr("Specified range requires two fields"); + + first = to_cron_int(parts[0]); + last = to_cron_int(parts[1]); + } + + if (first > maxval || last > maxval) + { + throw bad_cronexpr("Specified range exceeds maximum"); + } + if (first < minval || last < minval) + { + throw bad_cronexpr("Specified range is less than minimum"); + } + if (first > last) + { + throw bad_cronexpr("Specified range start exceeds range end"); + } + + return { first, last }; + } + + template + static void set_cron_field( + STRING_VIEW value, + std::bitset& target, + cron_int const minval, + cron_int const maxval) + { + if(value.length() > 0 && value[value.length()-1] == ',') + throw bad_cronexpr("Value cannot end with comma"); + + auto fields = utils::split(value, ','); + if (fields.empty()) + throw bad_cronexpr("Expression parsing error"); + + for (auto const & field : fields) + { + if (!utils::contains(field, '/')) + { +#ifdef CRONCPP_IS_CPP17 + auto[first, last] = detail::make_range(field, minval, maxval); +#else + auto range = detail::make_range(field, minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + for (cron_int i = first - minval; i <= last - minval; ++i) + { + target.set(i); + } + } + else + { + auto parts = utils::split(field, '/'); + if (parts.size() != 2) + throw bad_cronexpr("Incrementer must have two fields"); + +#ifdef CRONCPP_IS_CPP17 + auto[first, last] = detail::make_range(parts[0], minval, maxval); +#else + auto range = detail::make_range(parts[0], minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + + if (!utils::contains(parts[0], '-')) + { + last = maxval; + } + + auto delta = detail::to_cron_int(parts[1]); + if(delta <= 0) + throw bad_cronexpr("Incrementer must be a positive value"); + + for (cron_int i = first - minval; i <= last - minval; i += delta) + { + target.set(i); + } + } + } + } + + template + static void set_cron_days_of_week( + std::string value, + std::bitset<7>& target) + { + auto days = utils::to_upper(value); + auto days_replaced = detail::replace_ordinals( + days, +#ifdef CRONCPP_IS_CPP17 + Traits::DAYS +#else + Traits::DAYS() +#endif + ); + + if (days_replaced.size() == 1 && days_replaced[0] == '?') + days_replaced[0] = '*'; + + set_cron_field( + days_replaced, + target, + Traits::CRON_MIN_DAYS_OF_WEEK, + Traits::CRON_MAX_DAYS_OF_WEEK); + } + + template + static void set_cron_days_of_month( + std::string value, + std::bitset<31>& target) + { + if (value.size() == 1 && value[0] == '?') + value[0] = '*'; + + set_cron_field( + value, + target, + Traits::CRON_MIN_DAYS_OF_MONTH, + Traits::CRON_MAX_DAYS_OF_MONTH); + } + + template + static void set_cron_month( + std::string value, + std::bitset<12>& target) + { + auto month = utils::to_upper(value); + auto month_replaced = replace_ordinals( + month, +#ifdef CRONCPP_IS_CPP17 + Traits::MONTHS +#else + Traits::MONTHS() +#endif + ); + + set_cron_field( + month_replaced, + target, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS); + } + + template + inline size_t next_set_bit( + std::bitset const & target, + size_t /*minimum*/, + size_t /*maximum*/, + size_t offset) + { + for (auto i = offset; i < N; ++i) + { + if (target.test(i)) return i; + } + + return INVALID_CRON_INDEX; + } + + inline void add_to_field( + std::tm& date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::second: + date.tm_sec += val; + break; + case cron_field::minute: + date.tm_min += val; + break; + case cron_field::hour_of_day: + date.tm_hour += val; + break; + case cron_field::day_of_week: + case cron_field::day_of_month: + date.tm_mday += val; + break; + case cron_field::month: + date.tm_mon += val; + break; + case cron_field::year: + date.tm_year += val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void set_field( + std::tm& date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::second: + date.tm_sec = val; + break; + case cron_field::minute: + date.tm_min = val; + break; + case cron_field::hour_of_day: + date.tm_hour = val; + break; + case cron_field::day_of_week: + date.tm_wday = val; + break; + case cron_field::day_of_month: + date.tm_mday = val; + break; + case cron_field::month: + date.tm_mon = val; + break; + case cron_field::year: + date.tm_year = val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_field( + std::tm& date, + cron_field const field) + { + switch (field) + { + case cron_field::second: + date.tm_sec = 0; + break; + case cron_field::minute: + date.tm_min = 0; + break; + case cron_field::hour_of_day: + date.tm_hour = 0; + break; + case cron_field::day_of_week: + date.tm_wday = 0; + break; + case cron_field::day_of_month: + date.tm_mday = 1; + break; + case cron_field::month: + date.tm_mon = 0; + break; + case cron_field::year: + date.tm_year = 0; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_all_fields( + std::tm& date, + std::bitset<7> const & marked_fields) + { + for (size_t i = 0; i < marked_fields.size(); ++i) + { + if (marked_fields.test(i)) + reset_field(date, static_cast(i)); + } + } + + inline void mark_field( + std::bitset<7> & orders, + cron_field const field) + { + if (!orders.test(static_cast(field))) + orders.set(static_cast(field)); + } + + template + static size_t find_next( + std::bitset const & target, + std::tm& date, + unsigned int const minimum, + unsigned int const maximum, + unsigned int const value, + cron_field const field, + cron_field const next_field, + std::bitset<7> const & marked_fields) + { + auto next_value = next_set_bit(target, minimum, maximum, value); + if (INVALID_CRON_INDEX == next_value) + { + add_to_field(date, next_field, 1); + reset_field(date, field); + next_value = next_set_bit(target, minimum, maximum, 0); + } + + if (INVALID_CRON_INDEX == next_value || next_value != value) + { + set_field(date, field, static_cast(next_value)); + reset_all_fields(date, marked_fields); + } + + return next_value; + } + + template + static size_t find_next_day( + std::tm& date, + std::bitset<31> const & days_of_month, + size_t day_of_month, + std::bitset<7> const & days_of_week, + size_t day_of_week, + std::bitset<7> const & marked_fields) + { + unsigned int count = 0; + unsigned int maximum = 366; + while ( + (!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || + !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) + && count++ < maximum) + { + add_to_field(date, cron_field::day_of_month, 1); + + day_of_month = date.tm_mday; + day_of_week = date.tm_wday; + + reset_all_fields(date, marked_fields); + } + + return day_of_month; + } + + template + static bool find_next(cronexpr const & cex, + std::tm& date, + size_t const dot) + { + bool res = true; + + std::bitset<7> marked_fields{ 0 }; + std::bitset<7> empty_list{ 0 }; + + unsigned int second = date.tm_sec; + auto updated_second = find_next( + cex.seconds, + date, + Traits::CRON_MIN_SECONDS, + Traits::CRON_MAX_SECONDS, + second, + cron_field::second, + cron_field::minute, + empty_list); + + if (second == updated_second) + { + mark_field(marked_fields, cron_field::second); + } + + unsigned int minute = date.tm_min; + auto update_minute = find_next( + cex.minutes, + date, + Traits::CRON_MIN_MINUTES, + Traits::CRON_MAX_MINUTES, + minute, + cron_field::minute, + cron_field::hour_of_day, + marked_fields); + if (minute == update_minute) + { + mark_field(marked_fields, cron_field::minute); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int hour = date.tm_hour; + auto updated_hour = find_next( + cex.hours, + date, + Traits::CRON_MIN_HOURS, + Traits::CRON_MAX_HOURS, + hour, + cron_field::hour_of_day, + cron_field::day_of_week, + marked_fields); + if (hour == updated_hour) + { + mark_field(marked_fields, cron_field::hour_of_day); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int day_of_week = date.tm_wday; + unsigned int day_of_month = date.tm_mday; + auto updated_day_of_month = find_next_day( + date, + cex.days_of_month, + day_of_month, + cex.days_of_week, + day_of_week, + marked_fields); + if (day_of_month == updated_day_of_month) + { + mark_field(marked_fields, cron_field::day_of_month); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int month = date.tm_mon; + auto updated_month = find_next( + cex.months, + date, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS, + month, + cron_field::month, + cron_field::year, + marked_fields); + if (month != updated_month) + { + if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) + return false; + + res = find_next(cex, date, dot); + if (!res) return res; + } + + return res; + } + } + + template + static cronexpr make_cron(STRING_VIEW expr) + { + cronexpr cex; + + if (expr.empty()) + throw bad_cronexpr("Invalid empty cron expression"); + + auto fields = utils::split(expr, ' '); + fields.erase( + std::remove_if(std::begin(fields), std::end(fields), + [](STRING_VIEW s) {return s.empty(); }), + std::end(fields)); + if (fields.size() != 6) + throw bad_cronexpr("cron expression must have six fields"); + + detail::set_cron_field(fields[0], cex.seconds, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS); + detail::set_cron_field(fields[1], cex.minutes, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES); + detail::set_cron_field(fields[2], cex.hours, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS); + + detail::set_cron_days_of_week(fields[5], cex.days_of_week); + + detail::set_cron_days_of_month(fields[3], cex.days_of_month); + + detail::set_cron_month(fields[4], cex.months); + + return cex; + } + + template + static std::tm cron_next(cronexpr const & cex, std::tm date) + { + time_t original = utils::tm_to_time(date); + if (INVALID_TIME == original) return {}; + + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + + time_t calculated = utils::tm_to_time(date); + if (INVALID_TIME == calculated) return {}; + + if (calculated == original) + { + add_to_field(date, detail::cron_field::second, 1); + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + } + + return date; + } + + template + static std::time_t cron_next(cronexpr const & cex, std::time_t const & date) + { + std::tm val; + std::tm* dt = utils::time_to_tm(&date, &val); + if (dt == nullptr) return INVALID_TIME; + + time_t original = utils::tm_to_time(*dt); + if (INVALID_TIME == original) return INVALID_TIME; + + if(!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + + time_t calculated = utils::tm_to_time(*dt); + if (INVALID_TIME == calculated) return calculated; + + if (calculated == original) + { + add_to_field(*dt, detail::cron_field::second, 1); + if(!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + } + + return utils::tm_to_time(*dt); + } +} diff --git a/common/database_schema.h b/common/database_schema.h index 95a550d69..eb539cdb9 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -328,7 +328,7 @@ namespace DatabaseSchema { "reports", "respawn_times", "saylink", - + "server_scheduled_events", }; } diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 368d8fbec..ad43ee61d 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -127,6 +127,7 @@ void EQEmuLogSys::LoadLogSettingsDefaults() log_settings[Logs::HotReload].log_to_gmsay = static_cast(Logs::General); log_settings[Logs::HotReload].log_to_console = static_cast(Logs::General); log_settings[Logs::Loot].log_to_gmsay = static_cast(Logs::General); + log_settings[Logs::Scheduler].log_to_console = static_cast(Logs::General); /** * RFC 5424 @@ -206,11 +207,7 @@ std::string EQEmuLogSys::FormatOutMessageString( const std::string &in_message ) { - std::string return_string; - - if (IsRfc5424LogCategory(log_category)) { - return_string = "[" + GetPlatformName() + "] "; - } + std::string return_string = "[" + GetPlatformName() + "] "; return return_string + "[" + Logs::LogCategoryName[log_category] + "] " + in_message; } diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index d28478a97..0b33b55a3 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -120,6 +120,7 @@ namespace Logs { Loot, Expeditions, DynamicZones, + Scheduler, MaxCategoryID /* Don't Remove this */ }; @@ -199,6 +200,7 @@ namespace Logs { "Loot", "Expeditions", "DynamicZones", + "Scheduler", }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index e32b43b4f..ddd27335f 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -636,6 +636,16 @@ OutF(LogSys, Logs::Detail, Logs::DynamicZones, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogScheduler(message, ...) do {\ + if (LogSys.log_settings[Logs::Scheduler].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Scheduler, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogSchedulerDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Scheduler].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Scheduler, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/platform.cpp b/common/platform.cpp index b67267155..e5481c339 100644 --- a/common/platform.cpp +++ b/common/platform.cpp @@ -46,15 +46,15 @@ std::string GetPlatformName() { switch (GetExecutablePlatformInt()) { case EQEmuExePlatform::ExePlatformWorld: - return "WorldServer"; + return "World"; case EQEmuExePlatform::ExePlatformQueryServ: - return "QueryServer"; + return "QS"; case EQEmuExePlatform::ExePlatformZone: - return "ZoneServer"; + return "Zone"; case EQEmuExePlatform::ExePlatformUCS: return "UCS"; case EQEmuExePlatform::ExePlatformLogin: - return "LoginServer"; + return "Login"; case EQEmuExePlatform::ExePlatformSocket_Server: return "SocketServer"; case EQEmuExePlatform::ExePlatformSharedMemory: @@ -70,4 +70,4 @@ std::string GetPlatformName() default: return ""; } -} \ No newline at end of file +} diff --git a/common/repositories/base/base_server_scheduled_events_repository.h b/common/repositories/base/base_server_scheduled_events_repository.h new file mode 100644 index 000000000..1cc319d6a --- /dev/null +++ b/common/repositories/base/base_server_scheduled_events_repository.h @@ -0,0 +1,427 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories + */ + +#ifndef EQEMU_BASE_SERVER_SCHEDULED_EVENTS_REPOSITORY_H +#define EQEMU_BASE_SERVER_SCHEDULED_EVENTS_REPOSITORY_H + +#include "../../database.h" +#include "../../string_util.h" + +class BaseServerScheduledEventsRepository { +public: + struct ServerScheduledEvents { + int id; + std::string description; + std::string event_type; + std::string event_data; + int minute_start; + int hour_start; + int day_start; + int month_start; + int year_start; + int minute_end; + int hour_end; + int day_end; + int month_end; + int year_end; + std::string cron_expression; + std::string created_at; + std::string deleted_at; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "description", + "event_type", + "event_data", + "minute_start", + "hour_start", + "day_start", + "month_start", + "year_start", + "minute_end", + "hour_end", + "day_end", + "month_end", + "year_end", + "cron_expression", + "created_at", + "deleted_at", + }; + } + + static std::string ColumnsRaw() + { + return std::string(implode(", ", Columns())); + } + + static std::string TableName() + { + return std::string("server_scheduled_events"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + ColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static ServerScheduledEvents NewEntity() + { + ServerScheduledEvents entry{}; + + entry.id = 0; + entry.description = ""; + entry.event_type = ""; + entry.event_data = ""; + entry.minute_start = 0; + entry.hour_start = 0; + entry.day_start = 0; + entry.month_start = 0; + entry.year_start = 0; + entry.minute_end = 0; + entry.hour_end = 0; + entry.day_end = 0; + entry.month_end = 0; + entry.year_end = 0; + entry.cron_expression = ""; + entry.created_at = ""; + entry.deleted_at = ""; + + return entry; + } + + static ServerScheduledEvents GetServerScheduledEventsEntry( + const std::vector &server_scheduled_eventss, + int server_scheduled_events_id + ) + { + for (auto &server_scheduled_events : server_scheduled_eventss) { + if (server_scheduled_events.id == server_scheduled_events_id) { + return server_scheduled_events; + } + } + + return NewEntity(); + } + + static ServerScheduledEvents FindOne( + Database& db, + int server_scheduled_events_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE id = {} LIMIT 1", + BaseSelect(), + server_scheduled_events_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + ServerScheduledEvents entry{}; + + entry.id = atoi(row[0]); + entry.description = row[1] ? row[1] : ""; + entry.event_type = row[2] ? row[2] : ""; + entry.event_data = row[3] ? row[3] : ""; + entry.minute_start = atoi(row[4]); + entry.hour_start = atoi(row[5]); + entry.day_start = atoi(row[6]); + entry.month_start = atoi(row[7]); + entry.year_start = atoi(row[8]); + entry.minute_end = atoi(row[9]); + entry.hour_end = atoi(row[10]); + entry.day_end = atoi(row[11]); + entry.month_end = atoi(row[12]); + entry.year_end = atoi(row[13]); + entry.cron_expression = row[14] ? row[14] : ""; + entry.created_at = row[15] ? row[15] : ""; + entry.deleted_at = row[16] ? row[16] : ""; + + return entry; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int server_scheduled_events_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + server_scheduled_events_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + ServerScheduledEvents server_scheduled_events_entry + ) + { + std::vector update_values; + + auto columns = Columns(); + + update_values.push_back(columns[1] + " = '" + EscapeString(server_scheduled_events_entry.description) + "'"); + update_values.push_back(columns[2] + " = '" + EscapeString(server_scheduled_events_entry.event_type) + "'"); + update_values.push_back(columns[3] + " = '" + EscapeString(server_scheduled_events_entry.event_data) + "'"); + update_values.push_back(columns[4] + " = " + std::to_string(server_scheduled_events_entry.minute_start)); + update_values.push_back(columns[5] + " = " + std::to_string(server_scheduled_events_entry.hour_start)); + update_values.push_back(columns[6] + " = " + std::to_string(server_scheduled_events_entry.day_start)); + update_values.push_back(columns[7] + " = " + std::to_string(server_scheduled_events_entry.month_start)); + update_values.push_back(columns[8] + " = " + std::to_string(server_scheduled_events_entry.year_start)); + update_values.push_back(columns[9] + " = " + std::to_string(server_scheduled_events_entry.minute_end)); + update_values.push_back(columns[10] + " = " + std::to_string(server_scheduled_events_entry.hour_end)); + update_values.push_back(columns[11] + " = " + std::to_string(server_scheduled_events_entry.day_end)); + update_values.push_back(columns[12] + " = " + std::to_string(server_scheduled_events_entry.month_end)); + update_values.push_back(columns[13] + " = " + std::to_string(server_scheduled_events_entry.year_end)); + update_values.push_back(columns[14] + " = '" + EscapeString(server_scheduled_events_entry.cron_expression) + "'"); + update_values.push_back(columns[15] + " = '" + EscapeString(server_scheduled_events_entry.created_at) + "'"); + update_values.push_back(columns[16] + " = '" + EscapeString(server_scheduled_events_entry.deleted_at) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + implode(", ", update_values), + PrimaryKey(), + server_scheduled_events_entry.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static ServerScheduledEvents InsertOne( + Database& db, + ServerScheduledEvents server_scheduled_events_entry + ) + { + std::vector insert_values; + + insert_values.push_back(std::to_string(server_scheduled_events_entry.id)); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.description) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.event_type) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.event_data) + "'"); + insert_values.push_back(std::to_string(server_scheduled_events_entry.minute_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.hour_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.day_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.month_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.year_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.minute_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.hour_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.day_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.month_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.year_end)); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.cron_expression) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.created_at) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.deleted_at) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + implode(",", insert_values) + ) + ); + + if (results.Success()) { + server_scheduled_events_entry.id = results.LastInsertedID(); + return server_scheduled_events_entry; + } + + server_scheduled_events_entry = NewEntity(); + + return server_scheduled_events_entry; + } + + static int InsertMany( + Database& db, + std::vector server_scheduled_events_entries + ) + { + std::vector insert_chunks; + + for (auto &server_scheduled_events_entry: server_scheduled_events_entries) { + std::vector insert_values; + + insert_values.push_back(std::to_string(server_scheduled_events_entry.id)); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.description) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.event_type) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.event_data) + "'"); + insert_values.push_back(std::to_string(server_scheduled_events_entry.minute_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.hour_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.day_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.month_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.year_start)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.minute_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.hour_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.day_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.month_end)); + insert_values.push_back(std::to_string(server_scheduled_events_entry.year_end)); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.cron_expression) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.created_at) + "'"); + insert_values.push_back("'" + EscapeString(server_scheduled_events_entry.deleted_at) + "'"); + + insert_chunks.push_back("(" + implode(",", insert_values) + ")"); + } + + std::vector insert_values; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ServerScheduledEvents entry{}; + + entry.id = atoi(row[0]); + entry.description = row[1] ? row[1] : ""; + entry.event_type = row[2] ? row[2] : ""; + entry.event_data = row[3] ? row[3] : ""; + entry.minute_start = atoi(row[4]); + entry.hour_start = atoi(row[5]); + entry.day_start = atoi(row[6]); + entry.month_start = atoi(row[7]); + entry.year_start = atoi(row[8]); + entry.minute_end = atoi(row[9]); + entry.hour_end = atoi(row[10]); + entry.day_end = atoi(row[11]); + entry.month_end = atoi(row[12]); + entry.year_end = atoi(row[13]); + entry.cron_expression = row[14] ? row[14] : ""; + entry.created_at = row[15] ? row[15] : ""; + entry.deleted_at = row[16] ? row[16] : ""; + + all_entries.push_back(entry); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, std::string where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ServerScheduledEvents entry{}; + + entry.id = atoi(row[0]); + entry.description = row[1] ? row[1] : ""; + entry.event_type = row[2] ? row[2] : ""; + entry.event_data = row[3] ? row[3] : ""; + entry.minute_start = atoi(row[4]); + entry.hour_start = atoi(row[5]); + entry.day_start = atoi(row[6]); + entry.month_start = atoi(row[7]); + entry.year_start = atoi(row[8]); + entry.minute_end = atoi(row[9]); + entry.hour_end = atoi(row[10]); + entry.day_end = atoi(row[11]); + entry.month_end = atoi(row[12]); + entry.year_end = atoi(row[13]); + entry.cron_expression = row[14] ? row[14] : ""; + entry.created_at = row[15] ? row[15] : ""; + entry.deleted_at = row[16] ? row[16] : ""; + + all_entries.push_back(entry); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, std::string where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + +}; + +#endif //EQEMU_BASE_SERVER_SCHEDULED_EVENTS_REPOSITORY_H diff --git a/common/repositories/server_scheduled_events_repository.h b/common/repositories/server_scheduled_events_repository.h new file mode 100644 index 000000000..edcc21dbc --- /dev/null +++ b/common/repositories/server_scheduled_events_repository.h @@ -0,0 +1,70 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * 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; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_SERVER_SCHEDULED_EVENTS_REPOSITORY_H +#define EQEMU_SERVER_SCHEDULED_EVENTS_REPOSITORY_H + +#include "../database.h" +#include "../string_util.h" +#include "base/base_server_scheduled_events_repository.h" + +class ServerScheduledEventsRepository: public BaseServerScheduledEventsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * ServerScheduledEventsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * ServerScheduledEventsRepository::GetWhereNeverExpires() + * ServerScheduledEventsRepository::GetWhereXAndY() + * ServerScheduledEventsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_SERVER_SCHEDULED_EVENTS_REPOSITORY_H diff --git a/common/server_event_scheduler.cpp b/common/server_event_scheduler.cpp new file mode 100644 index 000000000..1034eebfe --- /dev/null +++ b/common/server_event_scheduler.cpp @@ -0,0 +1,247 @@ +#include "../common/database.h" +#include "../common/string_util.h" +#include "server_event_scheduler.h" +#include "../common/cron/croncpp.h" +#include +#include +#include + +ServerEventScheduler::ServerEventScheduler() +{ + m_last_polled_minute = -1; + m_events = {}; + m_active_events = {}; +} + +ServerEventScheduler::~ServerEventScheduler() = default; + +void ServerEventScheduler::LoadScheduledEvents() +{ + if (!ValidateDatabaseConnection()) { + return; + } + + std::time_t time = std::time(nullptr); + std::tm *now = std::localtime(&time); + + m_events = ServerScheduledEventsRepository::GetWhere(*m_database, "deleted_at is null"); + for (auto &e: m_events) { + + auto start = BuildStartTimeFromEvent(e, now); + auto end = BuildEndTimeFromEvent(e, now); + + // data excluded from output because it can be very large + + LogScheduler( + "Loaded Event ({}) [{}] type [{}] start [{}/{}/{} {:02}:{:02}:00] end [{}/{}/{} {:02}:{:02}:00] cron [{}] created [{}]", + e.id, + e.description, + e.event_type, + start.tm_mon + 1, + start.tm_mday, + start.tm_year + 1900, + start.tm_hour, + start.tm_min, + end.tm_mon + 1, + end.tm_mday, + end.tm_year + 1900, + end.tm_hour, + end.tm_min, + e.cron_expression, + e.created_at + ); + } + + LogScheduler("Loaded scheduled events [{}]", m_events.size()); +} + +// checks to see if event is ready to be activated +bool ServerEventScheduler::ValidateEventReadyToActivate( + ServerScheduledEventsRepository::ServerScheduledEvents &e +) +{ + + // if there is a cron expression, it will try to parse it first before falling back to + // alternative time logic + if (!e.cron_expression.empty()) { + try { + auto cron = cron::make_cron(e.cron_expression); + std::time_t cron_now = std::time(nullptr); + std::time_t cron_next = cron::cron_next(cron, cron_now); + + // we have to pad our now window just a tad so we don't miss the cron window + if ((cron_now + 10) >= cron_next) { + LogScheduler("Cron time has been met! Event scheduling ({}) [{}]", e.id, e.description); + return true; + } + + LogSchedulerDetail("Cron now [{}] cron next [{}]\n", cron_now, cron_next); + } + catch (cron::bad_cronexpr const &ex) { + LogScheduler( + "Error: Cron expression error [{}] see [https://github.com/mariusbancila/croncpp#cron-expressions]", + ex.what() + ); + } + + return false; + } + + std::time_t time = std::time(nullptr); + std::tm *now = std::localtime(&time); + time_t now_time_unix = mktime(now); + auto start = BuildStartTimeFromEvent(e, now); + auto end = BuildEndTimeFromEvent(e, now); + time_t start_time_unix = mktime(&start); + + bool doesnt_end = ( + e.year_end == 0 && + e.month_end == 0 && + e.day_end == 0 && + e.hour_end == 0 && + e.minute_end == 0 + ); + + time_t end_time_unix; + if (!doesnt_end) { + end_time_unix = mktime(&end); + } + + if (now_time_unix >= start_time_unix && (doesnt_end || now_time_unix < end_time_unix)) { + LogSchedulerDetail( + "[ValidateEventReadyToActivate] now_time [{}] start_time [{}] doesnt_end [{}] end_time [{}]", + now_time_unix, + start_time_unix, + doesnt_end ? "true" : "false", + end_time_unix + ); + return true; + } + + return false; +} + +ServerEventScheduler *ServerEventScheduler::SetDatabase(Database *db) +{ + m_database = db; + + return this; +} + +bool ServerEventScheduler::ValidateDatabaseConnection() +{ + if (!m_database) { + LogError("[ServerEventScheduler::LoadScheduledEvents] No database connection"); + return false; + } + + return true; +} + +// in this function we simply look at events we have internally and events +// in the database and determine if any edits have been made +// this helps inform decisions to tell all zones to reload their events +bool ServerEventScheduler::CheckIfEventsChanged() +{ + auto events = ServerScheduledEventsRepository::GetWhere(*m_database, "deleted_at is null"); + + // first check if the size changed, if it did this is the easiest step + if (m_events.size() != events.size()) { + LogSchedulerDetail("[CheckIfEventsChanged] Event size has changed"); + m_events = events; + return true; + } + + // compare fields of database fields to internal events to see if any fields changed + for (auto &e: m_events) { + for (auto &dbe: events) { + if (dbe.id == e.id) { + if ( + dbe.description != e.description || + dbe.event_type != e.event_type || + dbe.event_data != e.event_data || + dbe.minute_start != e.minute_start || + dbe.hour_start != e.hour_start || + dbe.day_start != e.day_start || + dbe.month_start != e.month_start || + dbe.year_start != e.year_start || + dbe.minute_end != e.minute_end || + dbe.hour_end != e.hour_end || + dbe.day_end != e.day_end || + dbe.month_end != e.month_end || + dbe.year_end != e.year_end || + dbe.cron_expression != e.cron_expression || + dbe.created_at != e.created_at || + dbe.deleted_at != e.deleted_at + ) { + LogSchedulerDetail("[CheckIfEventsChanged] Field change detected"); + m_events = events; + return true; + } + } + } + } + + return false; +} + +// checks if event is active +bool ServerEventScheduler::IsEventActive(ServerScheduledEventsRepository::ServerScheduledEvents &e) +{ + for (auto &a: m_active_events) { + if (a.id == e.id) { + return true; + } + } + + return false; +} + +bool ServerEventScheduler::RemoveActiveEvent(ServerScheduledEventsRepository::ServerScheduledEvents &e) +{ + m_active_events.erase( + std::remove_if( + m_active_events.begin(), + m_active_events.end(), + [&](ServerScheduledEventsRepository::ServerScheduledEvents const &active_event) { + return active_event.id == e.id; + } + ), + m_active_events.end()); + + return false; +} + +std::tm ServerEventScheduler::BuildStartTimeFromEvent( + ServerScheduledEventsRepository::ServerScheduledEvents &e, + std::tm *now +) +{ + struct tm time{}; + time.tm_year = ((e.year_start > 0) ? e.year_start - 1900 : now->tm_year); + time.tm_mon = ((e.month_start > 0) ? e.month_start - 1 : now->tm_mon); + time.tm_mday = ((e.day_start > 0) ? e.day_start : now->tm_mday); + time.tm_hour = ((e.hour_start > 0) ? e.hour_start : now->tm_hour); + time.tm_min = ((e.minute_start > 0) ? e.minute_start : now->tm_min); + time.tm_sec = 0; + time.tm_isdst = now->tm_isdst; + + return time; +} + +std::tm ServerEventScheduler::BuildEndTimeFromEvent( + ServerScheduledEventsRepository::ServerScheduledEvents &e, + std::tm *now +) +{ + struct tm time{}; + time.tm_year = ((e.year_end > 0) ? e.year_end - 1900 : now->tm_year); + time.tm_mon = ((e.month_end > 0) ? e.month_end - 1 : now->tm_mon); + time.tm_mday = ((e.day_end > 0) ? e.day_end : now->tm_mday); + time.tm_hour = ((e.hour_end > 0) ? e.hour_end : now->tm_hour); + time.tm_min = ((e.minute_end > 0) ? e.minute_end : now->tm_min); + time.tm_sec = 0; + time.tm_isdst = now->tm_isdst; + + return time; +} diff --git a/common/server_event_scheduler.h b/common/server_event_scheduler.h new file mode 100644 index 000000000..5671eaf7c --- /dev/null +++ b/common/server_event_scheduler.h @@ -0,0 +1,57 @@ +#ifndef EQEMU_SERVER_EVENT_SCHEDULER_H +#define EQEMU_SERVER_EVENT_SCHEDULER_H + +#include "../common/repositories/server_scheduled_events_repository.h" +#include +#include + +namespace ServerEvents { + static const std::string EVENT_TYPE_HOT_ZONE_ACTIVE = "hot_zone_activate"; + static const std::string EVENT_TYPE_BROADCAST = "broadcast"; + static const std::string EVENT_TYPE_RELOAD_WORLD = "reload_world"; + static const std::string EVENT_TYPE_RULE_CHANGE = "rule_change"; + static const std::string EVENT_TYPE_CONTENT_FLAG_CHANGE = "content_flag_change"; +} + +class ServerEventScheduler { +public: + virtual ~ServerEventScheduler(); + ServerEventScheduler(); + ServerEventScheduler *SetDatabase(Database *db); + void LoadScheduledEvents(); + bool CheckIfEventsChanged(); + +protected: + + // events directly from the database + std::vector m_events; + + // used to track only when it is convenient to undo an action from an active event + // typically there should be two separate events to turn something on / off + // hotzones use this right now simply to keep us from toggling off the hotzone + // every minute we trigger and then immediately turning it right back on + std::vector m_active_events; + + // simple ticker used to determine when the last polled minute was so that when the minute + // changes we fire checking the scheduler + int m_last_polled_minute; + + // validates an event is currently active or not + bool ValidateEventReadyToActivate(ServerScheduledEventsRepository::ServerScheduledEvents &e); + + // is event active + bool IsEventActive(ServerScheduledEventsRepository::ServerScheduledEvents &e); + + // remove active event + bool RemoveActiveEvent(ServerScheduledEventsRepository::ServerScheduledEvents &e); + + // build time object from event + std::tm BuildStartTimeFromEvent(ServerScheduledEventsRepository::ServerScheduledEvents &e, tm *now); + std::tm BuildEndTimeFromEvent(ServerScheduledEventsRepository::ServerScheduledEvents &e, tm *now); + + // reference to database + Database *m_database; + bool ValidateDatabaseConnection(); +}; + +#endif //EQEMU_SERVER_EVENT_SCHEDULER_H diff --git a/common/servertalk.h b/common/servertalk.h index 384bcc15b..7fbac5f6e 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -222,6 +222,7 @@ #define ServerOP_UCSServerStatusRequest 0x4009 #define ServerOP_UCSServerStatusReply 0x4010 #define ServerOP_HotReloadQuests 0x4011 +#define ServerOP_UpdateSchedulerEvents 0x4012 #define ServerOP_CZCastSpellPlayer 0x4500 #define ServerOP_CZCastSpellGroup 0x4501 @@ -320,7 +321,7 @@ #define ServerOP_QSPlayerDropItem 0x5007 /* Query Serv Generic Packet Flag/Type Enumeration */ -enum { QSG_LFGuild = 0 }; +enum { QSG_LFGuild = 0 }; enum { QSG_LFGuild_PlayerMatches = 0, QSG_LFGuild_UpdatePlayerInfo, QSG_LFGuild_RequestPlayerInfo, QSG_LFGuild_UpdateGuildInfo, QSG_LFGuild_GuildMatches, QSG_LFGuild_RequestGuildInfo }; @@ -1933,7 +1934,7 @@ struct WWRemoveTask_Struct { uint32 task_id; uint8 min_status; uint8 max_status; - + }; struct WWResetActivity_Struct { diff --git a/common/version.h b/common/version.h index 56b4f3b0c..d66f8b060 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9161 +#define CURRENT_BINARY_DATABASE_VERSION 9162 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9027 diff --git a/utils/scripts/generators/repository-generator.pl b/utils/scripts/generators/repository-generator.pl index 838642b98..ce3246dad 100644 --- a/utils/scripts/generators/repository-generator.pl +++ b/utils/scripts/generators/repository-generator.pl @@ -237,7 +237,7 @@ foreach my $table_to_generate (@tables) { elsif ($column_default eq "''") { $default_value = '""'; } - elsif ((trim($column_default) eq "" || $column_default eq "NULL") && $column_type =~ /text|varchar/i) { + elsif ((trim($column_default) eq "" || $column_default eq "NULL") && $column_type =~ /text|varchar|datetime/i) { $default_value = '""'; } diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index df613531c..ba19f8c37 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -415,6 +415,7 @@ 9159|2020_12_22_expedition_system.sql|SELECT * FROM db_version WHERE version >= 9159|empty| 9160|2021_02_14_npc_exp_mod.sql|SHOW COLUMNS from `npc_types` LIKE 'exp_mod'|empty| 9161|2021_02_15_npc_spell_entries_unsigned.sql|SELECT * FROM db_version WHERE version >= 9161|empty| +9162|2021_02_17_server_scheduled_events|SELECT * FROM db_version WHERE version >= 9162|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2021_02_17_server_scheduled_events.sql b/utils/sql/git/required/2021_02_17_server_scheduled_events.sql new file mode 100644 index 000000000..deb69423f --- /dev/null +++ b/utils/sql/git/required/2021_02_17_server_scheduled_events.sql @@ -0,0 +1,21 @@ +CREATE TABLE `server_scheduled_events` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `description` varchar(255) DEFAULT NULL, + `event_type` varchar(100) DEFAULT NULL, + `event_data` text DEFAULT NULL, + `minute_start` int(11) DEFAULT 0, + `hour_start` int(11) DEFAULT 0, + `day_start` int(11) DEFAULT 0, + `month_start` int(11) DEFAULT 0, + `year_start` int(11) DEFAULT 0, + `minute_end` int(11) DEFAULT 0, + `hour_end` int(11) DEFAULT 0, + `day_end` int(11) DEFAULT 0, + `month_end` int(11) DEFAULT 0, + `year_end` int(11) DEFAULT 0, + `cron_expression` varchar(100) DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `deleted_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index 183f4a27d..9a7ef5750 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -25,6 +25,7 @@ SET(world_sources web_interface.cpp web_interface_eqw.cpp wguild_mgr.cpp + world_event_scheduler.cpp world_config.cpp world_console_connection.cpp world_server_command_handler.cpp @@ -65,6 +66,7 @@ SET(world_headers world_tcp_connection.h world_server_command_handler.h worlddb.h + world_event_scheduler.h world_store.h zonelist.h zoneserver.h diff --git a/world/main.cpp b/world/main.cpp index c5c67ab87..a094c8b4f 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -97,6 +97,7 @@ union semun { #include "../common/content/world_content_service.h" #include "../common/repositories/merchantlist_temp_repository.h" #include "world_store.h" +#include "world_event_scheduler.h" WorldStore world_store; ClientList client_list; @@ -107,6 +108,7 @@ UCSConnection UCSLink; QueryServConnection QSLink; LauncherList launcher_list; AdventureManager adventure_manager; +WorldEventScheduler event_scheduler; EQ::Random emu_random; volatile bool RunLoops = true; uint32 numclients = 0; @@ -442,6 +444,8 @@ int main(int argc, char** argv) { content_db.LoadCharacterCreateAllocations(); content_db.LoadCharacterCreateCombos(); + event_scheduler.SetDatabase(&database)->LoadScheduledEvents(); + std::unique_ptr console; if (Config->TelnetEnabled) { LogInfo("Console (TCP) listener started"); @@ -603,6 +607,8 @@ int main(int argc, char** argv) { } } + event_scheduler.Process(&zoneserver_list); + client_list.Process(); if (PurgeInstanceTimer.Check()) { diff --git a/world/world_event_scheduler.cpp b/world/world_event_scheduler.cpp new file mode 100644 index 000000000..444b91965 --- /dev/null +++ b/world/world_event_scheduler.cpp @@ -0,0 +1,64 @@ +#include "world_event_scheduler.h" +#include "../common/servertalk.h" + +void WorldEventScheduler::Process(ZSList *zs_list) +{ + std::time_t time = std::time(nullptr); + std::tm *now = std::localtime(&time); + + // once a minute polling + if (m_last_polled_minute != now->tm_min) { + + // refresh; world polls and tells zones if they should update if there is a change + if (CheckIfEventsChanged()) { + LogSchedulerDetail("Event changes detected, forcing zones to refresh their schedules..."); + auto pack = new ServerPacket(ServerOP_UpdateSchedulerEvents, 0); + zs_list->SendPacket(pack); + safe_delete(pack); + } + + int month = (now->tm_mon + 1); + int year = (now->tm_year + 1900); + + LogSchedulerDetail( + "Polling year [{}] month [{}] day [{}] hour [{}] minute [{}]", + year, + month, + now->tm_mday, + now->tm_hour, + now->tm_min + ); + + for (auto &e: m_events) { + + // discard uninteresting events as its less work to calculate time on events we don't care about + // different processes are interested in different events + if ( + e.event_type != ServerEvents::EVENT_TYPE_BROADCAST && + e.event_type != ServerEvents::EVENT_TYPE_RELOAD_WORLD + ) { + continue; + } + + // validate event is ready to activate and run it + if (ValidateEventReadyToActivate(e)) { + if (e.event_type == ServerEvents::EVENT_TYPE_BROADCAST) { + LogScheduler("Sending broadcast [{}]", e.event_data.c_str()); + zs_list->SendEmoteMessage(nullptr, 0, 0, 15, e.event_data.c_str()); + } + + if (e.event_type == ServerEvents::EVENT_TYPE_RELOAD_WORLD) { + LogScheduler("Sending reload world event [{}]", e.event_data.c_str()); + + auto pack = new ServerPacket(ServerOP_ReloadWorld, sizeof(ReloadWorld_Struct)); + auto *reload_world = (ReloadWorld_Struct *) pack->pBuffer; + reload_world->Option = 1; + zs_list->SendPacket(pack); + safe_delete(pack); + } + } + } + + m_last_polled_minute = now->tm_min; + } +} diff --git a/world/world_event_scheduler.h b/world/world_event_scheduler.h new file mode 100644 index 000000000..b2a6725df --- /dev/null +++ b/world/world_event_scheduler.h @@ -0,0 +1,12 @@ +#ifndef EQEMU_EVENT_SCHEDULER_H +#define EQEMU_EVENT_SCHEDULER_H + +#include "../common/server_event_scheduler.h" +#include "zonelist.h" + +class WorldEventScheduler : public ServerEventScheduler { +public: + void Process(ZSList *zs_list); +}; + +#endif //EQEMU_EVENT_SCHEDULER_H diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index d28c8b85b..0430274f9 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -152,6 +152,7 @@ SET(zone_sources zone.cpp zone_config.cpp zonedb.cpp + zone_event_scheduler.cpp zone_reload.cpp zone_store.cpp zoning.cpp) @@ -265,6 +266,7 @@ SET(zone_headers worldserver.h xtargetautohaters.h zone.h + zone_event_scheduler.h zone_config.h zonedb.h zonedump.h diff --git a/zone/main.cpp b/zone/main.cpp index dc708a517..b81a41f5b 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -78,6 +78,7 @@ #include #include "../common/unix.h" #include "zone_store.h" +#include "zone_event_scheduler.h" #endif @@ -100,8 +101,9 @@ QueryServ *QServ = 0; TaskManager *task_manager = 0; NpcScaleManager *npc_scale_manager; QuestParserCollection *parse = 0; -EQEmuLogSys LogSys; -WorldContentService content_service; +EQEmuLogSys LogSys; +ZoneEventScheduler event_scheduler; +WorldContentService content_service; const SPDat_Spell_Struct* spells; int32 SPDAT_RECORDS = -1; const ZoneConfig *Config; @@ -387,6 +389,8 @@ int main(int argc, char** argv) { ZoneStore::LoadContentFlags(); + event_scheduler.SetDatabase(&database)->LoadScheduledEvents(); + #ifdef BOTS LogInfo("Loading bot commands"); int botretval = bot_command_init(); @@ -430,6 +434,7 @@ int main(int argc, char** argv) { parse->ReloadQuests(); worldserver.Connect(); + worldserver.SetScheduler(&event_scheduler); Timer InterserverTimer(INTERSERVER_TIMER); // does MySQL pings and auto-reconnect #ifdef EQPROFILE @@ -541,11 +546,11 @@ int main(int argc, char** argv) { entity_list.CorpseProcess(); entity_list.TrapProcess(); entity_list.RaidProcess(); - entity_list.Process(); entity_list.MobProcess(); entity_list.BeaconProcess(); entity_list.EncounterProcess(); + event_scheduler.Process(zone, &content_service); if (zone) { if (!zone->Process()) { diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 1b4758ff2..65f43676b 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -55,7 +55,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "zone_config.h" #include "zone_reload.h" - extern EntityList entity_list; extern Zone* zone; extern volatile bool is_zone_loaded; @@ -2815,6 +2814,15 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } + case ServerOP_UpdateSchedulerEvents: { + LogScheduler("Received signal from world to update"); + if (m_zone_scheduler) { + m_zone_scheduler->LoadScheduledEvents(); + } + + break; + } + case ServerOP_HotReloadQuests: { if (!zone) { @@ -3301,3 +3309,13 @@ void WorldServer::OnKeepAlive(EQ::Timer *t) ServerPacket pack(ServerOP_KeepAlive, 0); SendPacket(&pack); } + +ZoneEventScheduler *WorldServer::GetScheduler() const +{ + return m_zone_scheduler; +} + +void WorldServer::SetScheduler(ZoneEventScheduler *scheduler) +{ + WorldServer::m_zone_scheduler = scheduler; +} diff --git a/zone/worldserver.h b/zone/worldserver.h index 1eee0b948..bcccb67b7 100644 --- a/zone/worldserver.h +++ b/zone/worldserver.h @@ -20,6 +20,7 @@ #include "../common/eq_packet_structs.h" #include "../common/net/servertalk_client_connection.h" +#include "zone_event_scheduler.h" class ServerPacket; class EQApplicationPacket; @@ -76,6 +77,11 @@ private: std::unique_ptr m_connection; std::unique_ptr m_keepalive; + + ZoneEventScheduler *m_zone_scheduler; +public: + ZoneEventScheduler *GetScheduler() const; + void SetScheduler(ZoneEventScheduler *scheduler); }; #endif diff --git a/zone/zone.cpp b/zone/zone.cpp index 5fd0d5706..b69e9f2f3 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -941,7 +941,6 @@ Zone::Zone(uint32 in_zoneid, uint32 in_instanceid, const char* in_short_name) spawn2_timer(1000), hot_reload_timer(1000), qglobal_purge_timer(30000), - hotzone_timer(120000), m_SafePoint(0.0f,0.0f,0.0f), m_Graveyard(0.0f,0.0f,0.0f,0.0f) { @@ -1582,8 +1581,6 @@ bool Zone::Process() { } } - if(hotzone_timer.Check()) { UpdateHotzone(); } - mMovementManager->Process(); return true; @@ -2602,19 +2599,9 @@ uint32 Zone::GetSpawnKillCount(uint32 in_spawnid) { return 0; } -void Zone::UpdateHotzone() +void Zone::SetIsHotzone(bool is_hotzone) { - std::string query = StringFormat("SELECT hotzone FROM zone WHERE short_name = '%s'", GetShortName()); - auto results = content_db.QueryDatabase(query); - if (!results.Success()) - return; - - if (results.RowCount() == 0) - return; - - auto row = results.begin(); - - is_hotzone = atoi(row[0]) == 0 ? false: true; + Zone::is_hotzone = is_hotzone; } void Zone::RequestUCSServerStatus() { diff --git a/zone/zone.h b/zone/zone.h index e02bfd8c2..8b2e6b57c 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -289,7 +289,6 @@ public: void SpawnConditionChanged(const SpawnCondition &c, int16 old_value); void SpawnStatus(Mob *client); void StartShutdownTimer(uint32 set_time = (RuleI(Zone, AutoShutdownDelay))); - void UpdateHotzone(); void UpdateQGlobal(uint32 qid, QGlobal newGlobal); void weatherSend(Client *client = nullptr); @@ -356,6 +355,7 @@ public: */ void mod_init(); void mod_repop(); + void SetIsHotzone(bool is_hotzone); private: bool allow_mercs; @@ -401,7 +401,6 @@ private: Timer *Weather_Timer; Timer autoshutdown_timer; Timer clientauth_timer; - Timer hotzone_timer; Timer initgrids_timer; Timer qglobal_purge_timer; ZoneSpellsBlocked *blocked_spells; diff --git a/zone/zone_event_scheduler.cpp b/zone/zone_event_scheduler.cpp new file mode 100644 index 000000000..a300a0489 --- /dev/null +++ b/zone/zone_event_scheduler.cpp @@ -0,0 +1,165 @@ +#include "zone_event_scheduler.h" +#include "../common/rulesys.h" + +void ZoneEventScheduler::Process(Zone *zone, WorldContentService *content_service) +{ + std::time_t time = std::time(nullptr); + std::tm *now = std::localtime(&time); + + // once a minute polling + if (m_last_polled_minute != now->tm_min) { + int month = (now->tm_mon + 1); + int year = (now->tm_year + 1900); + + LogSchedulerDetail( + "Polling year [{}] month [{}] day [{}] hour [{}] minute [{}]", + year, + month, + now->tm_mday, + now->tm_hour, + now->tm_min + ); + + // because stored active events could have a reference of time that has been changed since + // the time has been updated, we need to make sure we update internal fields so that + // the scheduler can properly end events if we set a new end date + SyncEventDataWithActiveEvents(); + + // active events + for (auto &e: m_active_events) { + LogSchedulerDetail("Looping active event [{}]", e.description); + + // if event becomes no longer active + if (!ValidateEventReadyToActivate(e)) { + LogSchedulerDetail("Looping active event validated [{}]", e.event_type); + if (e.event_type == ServerEvents::EVENT_TYPE_HOT_ZONE_ACTIVE) { + LogScheduler("Deactivating event [{}] disabling hotzone status", e.description); + for (auto &short_name: split(e.event_data, ',')) { + if (zone->GetShortName() == short_name) { + zone->SetIsHotzone(false); + break; + } + } + RemoveActiveEvent(e); + } + + if (e.event_type == ServerEvents::EVENT_TYPE_RULE_CHANGE) { + LogScheduler("Deactivating event [{}] resetting rules to normal", e.description); + RuleManager::Instance()->LoadRules(m_database, RuleManager::Instance()->GetActiveRuleset(), true); + + // force active events clear and reapply all active events because we reset the entire state + // ideally if we could revert only the state of which was originally set we would only remove one active event + m_active_events.clear(); + } + + if (e.event_type == ServerEvents::EVENT_TYPE_CONTENT_FLAG_CHANGE) { + auto flag_name = e.event_data; + if (!flag_name.empty()) { + LogScheduler("Deactivating event [{}] resetting content flags", e.description); + content_service->ReloadContentFlags(*m_database); + } + + // force active events clear and reapply all active events because we reset the entire state + // ideally if we could revert only the state of which was originally set we would only remove one active event + m_active_events.clear(); + } + } + } + + // check for active + for (auto &e: m_events) { + + // discard uninteresting events as its less work to calculate time on events we don't care about + // different processes are interested in different events + if ( + e.event_type != ServerEvents::EVENT_TYPE_HOT_ZONE_ACTIVE && + e.event_type != ServerEvents::EVENT_TYPE_CONTENT_FLAG_CHANGE && + e.event_type != ServerEvents::EVENT_TYPE_RULE_CHANGE + ) { + continue; + } + + // the scheduler as of today manipulates events in memory and is preferred to be that way + // the scheduler changes temporary "state" in the server for a period of time for things such as + // hotzone activation, content flag activation, rule value activation + // when these events expire, the events become untoggled in memory + // there can be support for one-time events that are more suitable to run from worlds scheduler + // such as broadcasts, reloads + if (ValidateEventReadyToActivate(e) && !IsEventActive(e)) { + if (e.event_type == ServerEvents::EVENT_TYPE_HOT_ZONE_ACTIVE) { + for (auto &short_name: split(e.event_data, ',')) { + if (zone->GetShortName() == short_name) { + zone->SetIsHotzone(true); + LogScheduler("Activating Event [{}] Enabling zone as hotzone", e.description); + break; + } + } + m_active_events.push_back(e); + } + + if (e.event_type == ServerEvents::EVENT_TYPE_RULE_CHANGE) { + auto params = split(e.event_data, '='); + auto rule_key = params[0]; + auto rule_value = params[1]; + if (!rule_key.empty() && !rule_value.empty()) { + LogScheduler( + "Activating Event [{}] scheduled rule change, setting rule [{}] to [{}]", + e.description, + rule_key, + rule_value + ); + RuleManager::Instance()->SetRule(rule_key.c_str(), rule_value.c_str(), nullptr, false, true); + } + m_active_events.push_back(e); + } + + if (e.event_type == ServerEvents::EVENT_TYPE_CONTENT_FLAG_CHANGE) { + auto flag_name = e.event_data; + if (!flag_name.empty()) { + LogScheduler( + "Activating Event [{}] scheduled content flag change, setting flag [{}] to enabled", + e.description, + flag_name + ); + + auto flags = content_service->GetContentFlags(); + flags.push_back(flag_name); + content_service->SetContentFlags(flags); + m_active_events.push_back(e); + } + } + } + } + + m_last_polled_minute = now->tm_min; + } +} + +// because stored active events could have a reference of time that has been changed since +// the time has been updated, we need to make sure we update internal fields so that +// the scheduler can properly end events if we set a new end date +void ZoneEventScheduler::SyncEventDataWithActiveEvents() +{ + for (auto &a: m_active_events) { + for (auto &e: m_events) { + if (e.id == a.id) { + a.description = e.description; + a.event_type = e.event_type; + a.event_data = e.event_data; + a.minute_start = e.minute_start; + a.hour_start = e.hour_start; + a.day_start = e.day_start; + a.month_start = e.month_start; + a.year_start = e.year_start; + a.minute_end = e.minute_end; + a.hour_end = e.hour_end; + a.day_end = e.day_end; + a.month_end = e.month_end; + a.year_end = e.year_end; + a.cron_expression = e.cron_expression; + a.created_at = e.created_at; + a.deleted_at = e.deleted_at; + } + } + } +} diff --git a/zone/zone_event_scheduler.h b/zone/zone_event_scheduler.h new file mode 100644 index 000000000..a2e6c67ad --- /dev/null +++ b/zone/zone_event_scheduler.h @@ -0,0 +1,14 @@ +#ifndef EQEMU_ZONE_EVENT_SCHEDULER_H +#define EQEMU_ZONE_EVENT_SCHEDULER_H + +#include "../common/server_event_scheduler.h" +#include "zone.h" +#include "../common/content/world_content_service.h" + +class ZoneEventScheduler : public ServerEventScheduler { +public: + void Process(Zone *zone, WorldContentService *content_service); + void SyncEventDataWithActiveEvents(); +}; + +#endif //EQEMU_ZONE_EVENT_SCHEDULER_H diff --git a/zone/zone_store.cpp b/zone/zone_store.cpp index 2a1b2da79..467c3af25 100644 --- a/zone/zone_store.cpp +++ b/zone/zone_store.cpp @@ -146,20 +146,7 @@ ZoneRepository::Zone ZoneStore::GetZone(const char *in_zone_name) */ void ZoneStore::LoadContentFlags() { - std::vector set_content_flags; - auto content_flags = ContentFlagsRepository::GetWhere(database, "enabled = 1"); - - set_content_flags.reserve(content_flags.size()); - for (auto &flags: content_flags) { - set_content_flags.push_back(flags.flag_name); - } - - LogInfo( - "Enabled content flags [{}]", - implode(", ", set_content_flags) - ); - - content_service.SetContentFlags(set_content_flags); + content_service.ReloadContentFlags(database); } /**