diff --git a/lua/bladw_announcement/server/sv_scheduler.lua b/lua/bladw_announcement/server/sv_scheduler.lua new file mode 100644 index 0000000..1b9c998 --- /dev/null +++ b/lua/bladw_announcement/server/sv_scheduler.lua @@ -0,0 +1,145 @@ +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)