8695419876
Implement a scheduler system for BladW announcements that supports both: - Interval-based scheduling (e.g., "30m", "90s", "2h", "1d") - Cron-based scheduling with standard 5-field format (minute hour day month weekday) The scheduler parses configuration from BladW.Announcement.Schedules and manages timers automatically, cleaning up old timers on reload. Cron expressions are validated and compiled into matcher functions for efficient execution checking every 20 seconds.
146 lines
4.8 KiB
Lua
146 lines
4.8 KiB
Lua
BladW = BladW or {}
|
|
BladW.Announcement = BladW.Announcement or {}
|
|
|
|
-- planificateur d'annonces (intervalle + cron), lit BladW.Announcement.Schedules
|
|
|
|
local TIMER_PREFIX = "BladW_Announcement_Sched_"
|
|
local CRON_TIMER = "BladW_Announcement_Cron"
|
|
|
|
-- je garde les noms des timers sur la table pour bien les nettoyer au reload
|
|
BladW.Announcement._timers = BladW.Announcement._timers or {}
|
|
|
|
local function Warn(msg)
|
|
MsgC(Color(255, 200, 50), "[BladW Annonce] " .. msg .. "\n")
|
|
end
|
|
|
|
-- "30m" / "90s" / "2h" / "1d" -> secondes
|
|
local function ParseInterval(str)
|
|
if isnumber(str) then return str end
|
|
if not isstring(str) then return nil end
|
|
|
|
local num = tonumber(string.match(str, "^%s*([%d%.]+)"))
|
|
if not num then return nil end
|
|
|
|
local unit = string.lower(string.match(str, "([smhd])%s*$") or "s")
|
|
local mult = ({ s = 1, m = 60, h = 3600, d = 86400 })[unit] or 1
|
|
return num * mult
|
|
end
|
|
|
|
-- un champ cron ("*", "*/5", "1-5", "1,3,5", "7") -> fonction(v) qui dit si ça matche
|
|
local function CompileField(field, lo, hi)
|
|
local ranges = {}
|
|
|
|
for part in string.gmatch(field, "[^,]+") do
|
|
local base, step = part, 1
|
|
|
|
local slash = string.find(part, "/", 1, true)
|
|
if slash then
|
|
base = string.sub(part, 1, slash - 1)
|
|
step = tonumber(string.sub(part, slash + 1)) or 1
|
|
end
|
|
|
|
local a, b
|
|
if base == "*" then
|
|
a, b = lo, hi
|
|
else
|
|
local dash = string.find(base, "-", 1, true)
|
|
if dash then
|
|
a = tonumber(string.sub(base, 1, dash - 1))
|
|
b = tonumber(string.sub(base, dash + 1))
|
|
else
|
|
a = tonumber(base)
|
|
b = a
|
|
end
|
|
end
|
|
|
|
if a and b then
|
|
ranges[#ranges + 1] = { a = a, b = b, step = math.max(step, 1) }
|
|
end
|
|
end
|
|
|
|
return function(v)
|
|
for _, r in ipairs(ranges) do
|
|
if v >= r.a and v <= r.b and ((v - r.a) % r.step == 0) then return true end
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- "min heure jour mois jour-semaine" -> les matchers, nil si c'est pas 5 champs
|
|
local function CompileCron(str)
|
|
local fields = {}
|
|
for tok in string.gmatch(str, "%S+") do fields[#fields + 1] = tok end
|
|
if #fields ~= 5 then return nil end
|
|
|
|
return {
|
|
min = CompileField(fields[1], 0, 59),
|
|
hour = CompileField(fields[2], 0, 23),
|
|
day = CompileField(fields[3], 1, 31),
|
|
mon = CompileField(fields[4], 1, 12),
|
|
wday = CompileField(fields[5], 0, 6), -- 0 = dimanche
|
|
}
|
|
end
|
|
|
|
local function CronMatches(c, t)
|
|
local wday = t.wday - 1 -- os.date : 1=dim..7=sam, moi je veux 0..6
|
|
return c.min(t.min) and c.hour(t.hour) and c.day(t.day) and c.mon(t.month) and c.wday(wday)
|
|
end
|
|
|
|
-- (re)fabrique tous les timers depuis la config
|
|
function BladW.Announcement.RebuildSchedules()
|
|
-- on vire les anciens
|
|
for _, name in ipairs(BladW.Announcement._timers) do timer.Remove(name) end
|
|
timer.Remove(CRON_TIMER)
|
|
BladW.Announcement._timers = {}
|
|
|
|
local schedules = BladW.Announcement.Schedules or {}
|
|
local crons = {}
|
|
|
|
for i, entry in ipairs(schedules) do
|
|
if entry.cron then
|
|
local compiled = CompileCron(entry.cron)
|
|
if compiled then
|
|
crons[#crons + 1] = { entry = entry, cron = compiled, lastMinute = -1 }
|
|
else
|
|
Warn('cron invalide (attendu "m h jm mo js") : "' .. tostring(entry.cron) .. '"')
|
|
end
|
|
|
|
elseif entry.every then
|
|
local secs = ParseInterval(entry.every)
|
|
if secs and secs > 0 then
|
|
local name = TIMER_PREFIX .. i
|
|
timer.Create(name, secs, 0, function()
|
|
BladW.Announcement.Broadcast(entry)
|
|
end)
|
|
BladW.Announcement._timers[#BladW.Announcement._timers + 1] = name
|
|
else
|
|
Warn('intervalle invalide (ex "30m") : "' .. tostring(entry.every) .. '"')
|
|
end
|
|
|
|
else
|
|
Warn("entrée #" .. i .. " ignorée : ni 'every' ni 'cron'.")
|
|
end
|
|
end
|
|
|
|
-- un seul timer check tous les cron (tick 20s, une fois par minute qui matche)
|
|
if #crons > 0 then
|
|
timer.Create(CRON_TIMER, 20, 0, function()
|
|
local now = os.time()
|
|
local minuteKey = math.floor(now / 60)
|
|
local t = os.date("*t", now)
|
|
|
|
for _, c in ipairs(crons) do
|
|
if c.lastMinute ~= minuteKey and CronMatches(c.cron, t) then
|
|
c.lastMinute = minuteKey
|
|
BladW.Announcement.Broadcast(c.entry)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- le config (shared) charge après server/, du coup j'attends une frame que Presets/Schedules soient là
|
|
timer.Simple(0, function()
|
|
BladW.Announcement.RebuildSchedules()
|
|
end)
|