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)