Files
bladwin-RP/lua/bladw_announcement/server/sv_scheduler.lua
T
nocode 8695419876 feat(scheduler): add announcement scheduler with interval and cron support
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.
2026-07-20 13:49:27 +02:00

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)