[Scheduler] Event scheduler implementation (#1257)

* Event scheduler implementation

* Create 2021_02_17_server_scheduled_events.sql

* Tweak

* Remove unused event [skip ci]

* Cleanup [skip ci]

* PR adjustments

* Database manifest
This commit is contained in:
Chris Miles 2021-03-29 02:52:57 -05:00 committed by GitHub
parent f51bc4daaf
commit 7aa5308f9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2053 additions and 50 deletions

View File

@ -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

View File

@ -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<std::string> 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);
}

View File

@ -24,6 +24,8 @@
#include <string>
#include <vector>
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<std::string> &GetContentFlags() const;
bool IsContentFlagEnabled(const std::string& content_flag);
void SetContentFlags(std::vector<std::string> content_flags);
void ReloadContentFlags(Database &db);
void SetExpansionContext();
};

876
common/cron/croncpp.h Normal file
View File

@ -0,0 +1,876 @@
#pragma once
#include <vector>
#include <string>
#include <sstream>
#include <bitset>
#include <cctype>
#include <ctime>
#include <iomanip>
#include <algorithm>
#if __cplusplus > 201402L
#include <string_view>
#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<std::time_t>(-1);
constexpr size_t INVALID_CRON_INDEX = static_cast<size_t>(-1);
class cronexpr;
namespace detail
{
enum class cron_field
{
second,
minute,
hour_of_day,
day_of_week,
day_of_month,
month,
year
};
template <typename Traits>
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<std::string> DAYS = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
static const inline std::vector<std::string> MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
#else
static std::vector<std::string>& DAYS()
{
static std::vector<std::string> days = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
return days;
}
static std::vector<std::string>& MONTHS()
{
static std::vector<std::string> 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<std::string> DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
static const inline std::vector<std::string> MONTHS = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
#else
static std::vector<std::string>& DAYS()
{
static std::vector<std::string> days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
return days;
}
static std::vector<std::string>& MONTHS()
{
static std::vector<std::string> 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<std::string> DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
static const inline std::vector<std::string> MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
#else
static std::vector<std::string>& DAYS()
{
static std::vector<std::string> days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
return days;
}
static std::vector<std::string>& MONTHS()
{
static std::vector<std::string> months = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
return months;
}
#endif
};
class cronexpr;
template <typename Traits = cron_standard_traits>
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 <typename Traits>
friend bool detail::find_next(cronexpr const & cex,
std::tm& date,
size_t const dot);
friend std::string to_string(cronexpr const & cex);
template <typename Traits>
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<int(*)(int)>(std::toupper));
return text;
}
static std::vector<std::string> split(STRING_VIEW text, char const delimiter)
{
std::vector<std::string> 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<cron_int>(std::stoul(text.data()));
}
catch (std::exception const & ex)
{
throw bad_cronexpr(ex.what());
}
}
static std::string replace_ordinals(
std::string text,
std::vector<std::string> 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<cron_int, cron_int> 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 <size_t N>
static void set_cron_field(
STRING_VIEW value,
std::bitset<N>& 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 <typename Traits>
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 <typename Traits>
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 <typename Traits>
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 <size_t N>
inline size_t next_set_bit(
std::bitset<N> 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<cron_field>(i));
}
}
inline void mark_field(
std::bitset<7> & orders,
cron_field const field)
{
if (!orders.test(static_cast<size_t>(field)))
orders.set(static_cast<size_t>(field));
}
template <size_t N>
static size_t find_next(
std::bitset<N> 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<int>(next_value));
reset_all_fields(date, marked_fields);
}
return next_value;
}
template <typename Traits>
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 <typename Traits>
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<Traits>(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<Traits>(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<Traits>(
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<Traits>(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<Traits>(cex, date, dot);
if (!res) return res;
}
return res;
}
}
template <typename Traits>
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<Traits>(fields[5], cex.days_of_week);
detail::set_cron_days_of_month<Traits>(fields[3], cex.days_of_month);
detail::set_cron_month<Traits>(fields[4], cex.months);
return cex;
}
template <typename Traits = cron_standard_traits>
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<Traits>(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<Traits>(cex, date, date.tm_year))
return {};
}
return date;
}
template <typename Traits = cron_standard_traits>
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<Traits>(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<Traits>(cex, *dt, dt->tm_year))
return INVALID_TIME;
}
return utils::tm_to_time(*dt);
}
}

View File

@ -328,7 +328,7 @@ namespace DatabaseSchema {
"reports",
"respawn_times",
"saylink",
"server_scheduled_events",
};
}

View File

@ -127,6 +127,7 @@ void EQEmuLogSys::LoadLogSettingsDefaults()
log_settings[Logs::HotReload].log_to_gmsay = static_cast<uint8>(Logs::General);
log_settings[Logs::HotReload].log_to_console = static_cast<uint8>(Logs::General);
log_settings[Logs::Loot].log_to_gmsay = static_cast<uint8>(Logs::General);
log_settings[Logs::Scheduler].log_to_console = static_cast<uint8>(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;
}

View File

@ -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",
};
}

View File

@ -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__);\

View File

@ -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 "";
}
}
}

View File

@ -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<std::string> 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<ServerScheduledEvents> &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<std::string> 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<std::string> 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<ServerScheduledEvents> server_scheduled_events_entries
)
{
std::vector<std::string> insert_chunks;
for (auto &server_scheduled_events_entry: server_scheduled_events_entries) {
std::vector<std::string> 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<std::string> insert_values;
auto results = db.QueryDatabase(
fmt::format(
"{} VALUES {}",
BaseInsert(),
implode(",", insert_chunks)
)
);
return (results.Success() ? results.RowsAffected() : 0);
}
static std::vector<ServerScheduledEvents> All(Database& db)
{
std::vector<ServerScheduledEvents> 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<ServerScheduledEvents> GetWhere(Database& db, std::string where_filter)
{
std::vector<ServerScheduledEvents> 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

View File

@ -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

View File

@ -0,0 +1,247 @@
#include "../common/database.h"
#include "../common/string_util.h"
#include "server_event_scheduler.h"
#include "../common/cron/croncpp.h"
#include <chrono>
#include <iostream>
#include <time.h>
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<cron::cron_standard_traits>(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;
}

View File

@ -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 <time.h>
#include <chrono>
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<ServerScheduledEventsRepository::ServerScheduledEvents> 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<ServerScheduledEventsRepository::ServerScheduledEvents> 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

View File

@ -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 {

View File

@ -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

View File

@ -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 = '""';
}

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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<EQ::Net::ConsoleServer> 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()) {

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -78,6 +78,7 @@
#include <pthread.h>
#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()) {

View File

@ -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;
}

View File

@ -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<EQ::Net::ServertalkClient> m_connection;
std::unique_ptr<EQ::Timer> m_keepalive;
ZoneEventScheduler *m_zone_scheduler;
public:
ZoneEventScheduler *GetScheduler() const;
void SetScheduler(ZoneEventScheduler *scheduler);
};
#endif

View File

@ -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() {

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -146,20 +146,7 @@ ZoneRepository::Zone ZoneStore::GetZone(const char *in_zone_name)
*/
void ZoneStore::LoadContentFlags()
{
std::vector<std::string> 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);
}
/**