Compare commits
22 Commits
main
...
Announcements
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc2835b11b
|
|||
|
cf747dd454
|
|||
|
2b56b15886
|
|||
|
6f579fb2db
|
|||
|
490d5a2194
|
|||
|
5f35c70286
|
|||
|
5fafde5e31
|
|||
|
e357313204
|
|||
|
05864e1ed4
|
|||
|
e40893a290
|
|||
|
b0ab2e3e31
|
|||
|
49a6a085af
|
|||
|
00eb696e7f
|
|||
|
2c5a73cf49
|
|||
|
8695419876
|
|||
|
fd5a9703bc
|
|||
|
7248b8c434
|
|||
|
904cf3566c
|
|||
|
b220f41c4a
|
|||
|
b183517e9f
|
|||
|
ba9e71e6ae
|
|||
|
f6ec6b1236
|
@@ -1,55 +1,340 @@
|
|||||||
xyghtBladWinRPMenu = xyghtBladWinRPMenu or {}
|
-- ============================================================
|
||||||
|
-- AUTOLOADER nocode — Dual-path récursif
|
||||||
|
-- Fichier : lua/autorun/bladWinRpMenu_loader.lua
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
AddCSLuaFile("bladw_deathscreen/config/sh_config.lua")
|
local NoCode = {}
|
||||||
|
|
||||||
if SERVER then
|
NoCode.config = {
|
||||||
AddCSLuaFile("bladw_hud/client/cl_hud.lua")
|
-- Dossier(s)-module(s) à charger.
|
||||||
AddCSLuaFile("bladw_cMenu/client/cl_cMenu.lua")
|
-- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
|
||||||
AddCSLuaFile("bladw_deathscreen/client/cl_interface_ds.lua")
|
-- • avec wildcard -> glob, charge TOUS ceux qui matchent :
|
||||||
|
-- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu)
|
||||||
|
-- • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé
|
||||||
|
root = "none",
|
||||||
|
-- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile),
|
||||||
|
-- jamais auto-inclus. Ils sont chargés à la demande via include("lib/...").
|
||||||
|
libs = { "lib" },
|
||||||
|
safe_load = true,
|
||||||
|
verbose = true,
|
||||||
|
exclude = { "template.lua", "disabled", "_dev" },
|
||||||
|
}
|
||||||
|
|
||||||
include("bladw_deathscreen/server/sv_hooks.lua")
|
-- ============================================================
|
||||||
|
-- Logs
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
local C = {
|
||||||
|
ok = Color(100, 220, 100),
|
||||||
|
err = Color(255, 80, 80),
|
||||||
|
info = Color(120, 180, 255),
|
||||||
|
warn = Color(255, 200, 50),
|
||||||
|
dim = Color(160, 160, 160),
|
||||||
|
title = Color(200, 140, 255),
|
||||||
|
}
|
||||||
|
|
||||||
|
local function Log(col, msg)
|
||||||
|
MsgC(col, msg .. "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Utilitaires
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
local function StartsWith(str, prefix)
|
||||||
|
return str:sub(1, #prefix) == prefix
|
||||||
|
end
|
||||||
|
|
||||||
|
local function IsExcluded(name)
|
||||||
|
for _, ex in ipairs(NoCode.config.exclude) do
|
||||||
|
if name == ex then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SortFiles(files)
|
||||||
|
table.sort(files, function(a, b)
|
||||||
|
local function rank(n)
|
||||||
|
if StartsWith(n, "sh_") then return 1 end
|
||||||
|
if StartsWith(n, "sv_") then return 2 end
|
||||||
|
if StartsWith(n, "cl_") then return 3 end
|
||||||
|
return 4
|
||||||
|
end
|
||||||
|
return rank(a) < rank(b)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Fusionne deux tables en évitant les doublons
|
||||||
|
local function Merge(t1, t2)
|
||||||
|
local seen = {}
|
||||||
|
local result = {}
|
||||||
|
for _, v in ipairs(t1 or {}) do
|
||||||
|
if not seen[v] then seen[v] = true; table.insert(result, v) end
|
||||||
|
end
|
||||||
|
for _, v in ipairs(t2 or {}) do
|
||||||
|
if not seen[v] then seen[v] = true; table.insert(result, v) end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Compteurs
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
local stats = { loaded = 0, sent = 0, errors = 0 }
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Chargement d'un fichier unique
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
local function LoadFile(path)
|
||||||
|
local filename = path:match("([^/]+)$")
|
||||||
|
local realm, shouldLoad, shouldSend
|
||||||
|
|
||||||
|
if StartsWith(filename, "sh_") then
|
||||||
|
realm = "SHARED"
|
||||||
|
shouldSend = SERVER
|
||||||
|
shouldLoad = true
|
||||||
|
|
||||||
|
elseif StartsWith(filename, "sv_") then
|
||||||
|
realm = "SERVER"
|
||||||
|
shouldLoad = SERVER
|
||||||
|
|
||||||
|
elseif StartsWith(filename, "cl_") then
|
||||||
|
realm = "CLIENT"
|
||||||
|
shouldSend = SERVER
|
||||||
|
shouldLoad = not SERVER
|
||||||
|
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-SemiBold.ttf")
|
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Medium.ttf")
|
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Bold.ttf")
|
|
||||||
resource.AddSingleFile("resource/fonts/Poppins-Regular.ttf")
|
|
||||||
else
|
else
|
||||||
include("bladw_hud/client/cl_hud.lua")
|
return
|
||||||
include("bladw_cMenu/client/cl_cMenu.lua")
|
end
|
||||||
include("bladw_deathscreen/client/cl_interface_ds.lua")
|
|
||||||
|
|
||||||
surface.CreateFont("bladw_text", {
|
if shouldSend then
|
||||||
font = "Poppins-SemiBold",
|
AddCSLuaFile(path)
|
||||||
size = 25,
|
stats.sent = stats.sent + 1
|
||||||
weight = 600,
|
if NoCode.config.verbose and not shouldLoad then
|
||||||
antialias = true
|
Log(C.info, (" [→] [%s] %s"):format(realm, path))
|
||||||
})
|
end
|
||||||
|
end
|
||||||
|
|
||||||
surface.CreateFont("bladw_text_Medium", {
|
if not shouldLoad then return end
|
||||||
font = "Poppins-SemiBold",
|
|
||||||
size = 20,
|
|
||||||
weight = 800,
|
|
||||||
antialias = true
|
|
||||||
})
|
|
||||||
|
|
||||||
surface.CreateFont("bladw_text_Bold", {
|
if NoCode.config.safe_load then
|
||||||
font = "Poppins-Bold",
|
local ok, err = pcall(include, path)
|
||||||
size = 70,
|
if ok then
|
||||||
weight = 700,
|
stats.loaded = stats.loaded + 1
|
||||||
antialias = true
|
if NoCode.config.verbose then
|
||||||
})
|
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
stats.errors = stats.errors + 1
|
||||||
|
Log(C.err, (" [✗] [%s] %s"):format(realm, path))
|
||||||
|
Log(C.err, (" ↳ %s"):format(tostring(err)))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
include(path)
|
||||||
|
stats.loaded = stats.loaded + 1
|
||||||
|
if NoCode.config.verbose then
|
||||||
|
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
surface.CreateFont("bladw_text_Regular", {
|
-- ============================================================
|
||||||
font = "Poppins-Regular",
|
-- Scan récursif — cherche TOUJOURS dans LUA et GAME en même temps
|
||||||
size = 35,
|
-- ============================================================
|
||||||
weight = 400,
|
|
||||||
antialias = true
|
|
||||||
})
|
|
||||||
|
|
||||||
surface.CreateFont("bladw_text2", {
|
local function LoadFolder(dir, depth)
|
||||||
font = "Poppins-SemiBold",
|
depth = depth or 0
|
||||||
size = 21.5,
|
|
||||||
weight = 800,
|
local basename = dir:match("([^/]+)$") or dir
|
||||||
antialias = true
|
if IsExcluded(basename) then
|
||||||
|
Log(C.warn, (" [~] Dossier ignoré : %s"):format(dir))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- On cherche dans les deux paths et on fusionne les résultats
|
||||||
|
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
|
||||||
|
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
|
||||||
|
|
||||||
|
local files = Merge(files1, files2)
|
||||||
|
local dirs = Merge(dirs1, dirs2)
|
||||||
|
|
||||||
|
if #files == 0 and #dirs == 0 then
|
||||||
|
if NoCode.config.verbose and depth > 0 then
|
||||||
|
Log(C.warn, (" [!] Dossier vide ou introuvable : %s"):format(dir))
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if NoCode.config.verbose and depth > 0 then
|
||||||
|
Log(C.dim, string.rep(" ", depth) .. "📂 " .. basename .. "/")
|
||||||
|
end
|
||||||
|
|
||||||
|
SortFiles(files)
|
||||||
|
|
||||||
|
for _, fname in ipairs(files) do
|
||||||
|
if fname:EndsWith(".lua") and not IsExcluded(fname) then
|
||||||
|
LoadFile(dir .. "/" .. fname)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Récursion sur tous les sous-dossiers sans limite de profondeur
|
||||||
|
for _, subdir in ipairs(dirs) do
|
||||||
|
if not IsExcluded(subdir) then
|
||||||
|
LoadFolder(dir .. "/" .. subdir, depth + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Envoi récursif d'une bibliothèque au client (sans l'inclure)
|
||||||
|
-- Les fichiers de lib/ ne suivent pas le préfixe sh_/sv_/cl_ ;
|
||||||
|
-- on les expédie simplement pour que include("lib/...") fonctionne.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
local function SendFolder(dir)
|
||||||
|
if not SERVER then return end
|
||||||
|
|
||||||
|
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
|
||||||
|
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
|
||||||
|
local files = Merge(files1, files2)
|
||||||
|
local dirs = Merge(dirs1, dirs2)
|
||||||
|
|
||||||
|
for _, fname in ipairs(files) do
|
||||||
|
if fname:EndsWith(".lua") and not IsExcluded(fname) then
|
||||||
|
local path = dir .. "/" .. fname
|
||||||
|
AddCSLuaFile(path)
|
||||||
|
stats.sent = stats.sent + 1
|
||||||
|
if NoCode.config.verbose then
|
||||||
|
Log(C.info, (" [→] [LIB] %s"):format(path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, subdir in ipairs(dirs) do
|
||||||
|
if not IsExcluded(subdir) then
|
||||||
|
SendFolder(dir .. "/" .. subdir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Découverte des dossiers-modules
|
||||||
|
-- (file.Find ne supporte le wildcard que sur le dernier segment,
|
||||||
|
-- donc on liste la racine lua/ puis on filtre nous-mêmes.)
|
||||||
|
-- • pattern sans "*" -> match exact (un seul dossier)
|
||||||
|
-- • pattern avec "*" -> glob converti en motif Lua (préfixe/suffixe/milieu)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
--- Échappe les caractères magiques d'un motif Lua.
|
||||||
|
local function EscapePattern(s)
|
||||||
|
return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Transforme un glob simple ("bladw_*") en motif Lua ancré ("^bladw_.*$").
|
||||||
|
local function GlobToPattern(glob)
|
||||||
|
return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FindRootFolders(pattern)
|
||||||
|
local roots = {}
|
||||||
|
|
||||||
|
-- Construit la fonction de test selon la présence ou non d'un wildcard.
|
||||||
|
local matches
|
||||||
|
if pattern:find("*", 1, true) then
|
||||||
|
local lua_pat = GlobToPattern(pattern)
|
||||||
|
matches = function(d) return d:match(lua_pat) ~= nil end
|
||||||
|
else
|
||||||
|
matches = function(d) return d == pattern end
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, dirs1 = file.Find("*", "LUA")
|
||||||
|
local _, dirs2 = file.Find("lua/*", "GAME")
|
||||||
|
|
||||||
|
for _, d in ipairs(Merge(dirs1, dirs2)) do
|
||||||
|
if matches(d) and not IsExcluded(d) then
|
||||||
|
table.insert(roots, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(roots)
|
||||||
|
return roots
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Polices client (Poppins)
|
||||||
|
-- Créées quel que soit le root, donc hors du flux de chargement.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
if CLIENT then
|
||||||
|
local fonts = {
|
||||||
|
{ "bladw_text", "Poppins-SemiBold", 25, 600 },
|
||||||
|
{ "bladw_text_Medium", "Poppins-SemiBold", 20, 800 },
|
||||||
|
{ "bladw_text_Bold", "Poppins-Bold", 70, 700 },
|
||||||
|
{ "bladw_text_Regular", "Poppins-Regular", 35, 400 },
|
||||||
|
{ "bladw_text2", "Poppins-SemiBold", 21.5, 800 },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f in ipairs(fonts) do
|
||||||
|
surface.CreateFont(f[1], {
|
||||||
|
font = f[2],
|
||||||
|
size = f[3],
|
||||||
|
weight = f[4],
|
||||||
|
antialias = true,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Point d'entrée
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
Log(C.title, "")
|
||||||
|
Log(C.title, " ╔══════════════════════════════════════╗")
|
||||||
|
Log(C.title, " ║ NoCode — Autoloader ║")
|
||||||
|
Log(C.title, " ╚══════════════════════════════════════╝")
|
||||||
|
|
||||||
|
local rootPattern = NoCode.config.root
|
||||||
|
|
||||||
|
-- root "none" (ou vide/nil) -> loader désactivé, on ne charge rien
|
||||||
|
if not rootPattern or rootPattern == "" or rootPattern:lower() == "none" then
|
||||||
|
Log(C.warn, " Root = none -> autoloader désactivé, rien n'est chargé.")
|
||||||
|
Log(C.dim, "")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log(C.info, " Dossier racine : lua/" .. rootPattern)
|
||||||
|
Log(C.dim, "")
|
||||||
|
|
||||||
|
-- 1) Expédie les bibliothèques partagées au client (rndx, etc.)
|
||||||
|
for _, lib in ipairs(NoCode.config.libs or {}) do
|
||||||
|
SendFolder(lib)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2) Charge chaque module racine correspondant au préfixe
|
||||||
|
local roots = FindRootFolders(rootPattern)
|
||||||
|
|
||||||
|
if #roots == 0 then
|
||||||
|
Log(C.warn, (" [!] Aucun module trouvé pour le préfixe : %s"):format(rootPattern))
|
||||||
|
else
|
||||||
|
for _, root in ipairs(roots) do
|
||||||
|
if NoCode.config.verbose then
|
||||||
|
Log(C.title, " ▶ " .. root)
|
||||||
|
end
|
||||||
|
LoadFolder(root)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Log(C.dim, "")
|
||||||
|
Log(C.title, " ╔══════════════════════════════════════╗")
|
||||||
|
if SERVER then
|
||||||
|
Log(C.ok, (" ║ ✓ %d chargé(s) / %d envoyé(s) au client"):format(stats.loaded, stats.sent))
|
||||||
|
else
|
||||||
|
Log(C.ok, (" ║ ✓ %d fichier(s) chargé(s) [CLIENT]"):format(stats.loaded))
|
||||||
|
end
|
||||||
|
if stats.errors > 0 then
|
||||||
|
Log(C.err, (" ║ ✗ %d erreur(s)"):format(stats.errors))
|
||||||
|
end
|
||||||
|
Log(C.title, " ╚══════════════════════════════════════╝")
|
||||||
|
Log(C.dim, "")
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
local rndx = include("lib/rndx.lua")
|
||||||
|
local logo = Material("nocode/announcement/logo.png")
|
||||||
|
|
||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
-- mêmes couleurs que les annonces
|
||||||
|
local COL_BG = Color(0, 0, 0, 200)
|
||||||
|
local COL_PANEL = Color(0, 0, 0, 160)
|
||||||
|
local COL_ACCENT = Color(76, 119, 200)
|
||||||
|
local COL_ACCENT_DIM = Color(60, 90, 150)
|
||||||
|
local COL_TEXT = Color(255, 255, 255)
|
||||||
|
local COL_RED = Color(255, 80, 80)
|
||||||
|
local COL_GRAY = Color(54, 54, 54, 255)
|
||||||
|
local COL_MUTED = Color(160, 160, 160)
|
||||||
|
local COL_WARN = Color(255, 200, 50)
|
||||||
|
local COL_LINE = Color(255, 255, 255, 60)
|
||||||
|
|
||||||
|
-- nb de caractères d'un champ (UTF-8, un accent = 1)
|
||||||
|
local function EntryLen(entry)
|
||||||
|
local v = entry:GetValue()
|
||||||
|
return utf8.len(v) or #v
|
||||||
|
end
|
||||||
|
|
||||||
|
-- petit triangle warning dessiné à la main (comme ça pas besoin de font emoji)
|
||||||
|
local function DrawWarning(x, y, size)
|
||||||
|
draw.NoTexture()
|
||||||
|
surface.SetDrawColor(COL_WARN)
|
||||||
|
surface.DrawPoly({
|
||||||
|
{ x = x + size * 0.5, y = y },
|
||||||
|
{ x = x + size, y = y + size },
|
||||||
|
{ x = x, y = y + size },
|
||||||
|
})
|
||||||
|
draw.SimpleText("!", "DermaDefaultBold", x + size * 0.5, y + size * 0.62, Color(30, 30, 30), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- mon style pour les champs de texte
|
||||||
|
local function StyleEntry(entry, placeholder, default)
|
||||||
|
entry:SetFont("bladw_text_Medium")
|
||||||
|
entry:SetText(default or "")
|
||||||
|
entry:SetPlaceholderText(placeholder or "")
|
||||||
|
entry:SetTextColor(COL_TEXT)
|
||||||
|
entry:SetPaintBackground(false)
|
||||||
|
entry.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
local outline = self:IsEditing() and COL_ACCENT or COL_LINE
|
||||||
|
rndx.DrawOutlined(10, 0, 0, w, h, outline, 2, rndx.SHAPE_FIGMA)
|
||||||
|
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- petite flèche bas
|
||||||
|
local function DrawArrow(w, h)
|
||||||
|
draw.NoTexture()
|
||||||
|
surface.SetDrawColor(COL_MUTED)
|
||||||
|
local ax = w - 24
|
||||||
|
surface.DrawPoly({
|
||||||
|
{ x = ax, y = h * 0.5 - 2 },
|
||||||
|
{ x = ax + 10, y = h * 0.5 - 2 },
|
||||||
|
{ x = ax + 5, y = h * 0.5 + 4 },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local LIST_H = 130 -- hauteur du panneau de cases une fois ouvert
|
||||||
|
local LIST_Y = 440 -- haut du panneau
|
||||||
|
|
||||||
|
local function OpenAAdminMenu()
|
||||||
|
|
||||||
|
-- si le menu est déjà ouvert je le vire
|
||||||
|
if IsValid(BladW_AdminMenu) then BladW_AdminMenu:Remove() end
|
||||||
|
|
||||||
|
local selectedPreset = ""
|
||||||
|
local maxLen = (BladW.Announcement.Conf or {}).announcement_max_length or 120
|
||||||
|
|
||||||
|
-- destinataires : scope + sélection multiple
|
||||||
|
local scope, scopeLabel = "all", "Tout le monde"
|
||||||
|
local selGroups, selPlayers = {}, {}
|
||||||
|
local expand = 0 -- 0 = replié, 1 = ouvert (animé)
|
||||||
|
|
||||||
|
local frame = vgui.Create("DFrame")
|
||||||
|
BladW_AdminMenu = frame
|
||||||
|
frame:SetSize(500, 640) -- taille max -> sert au centrage, on démarre replié
|
||||||
|
frame:Center()
|
||||||
|
frame:SetTall(498)
|
||||||
|
frame:SetTitle("")
|
||||||
|
frame:ShowCloseButton(false)
|
||||||
|
frame:MakePopup()
|
||||||
|
|
||||||
|
frame.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(20, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
|
||||||
|
rndx.Draw(20, 0, 0, w, h, COL_BG, rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(20, 0, 0, w, h, COL_TEXT, 3, rndx.SHAPE_FIGMA)
|
||||||
|
|
||||||
|
rndx.DrawMaterial(0, w * 0.5 - 30, 20, 60, 60, COL_TEXT, logo)
|
||||||
|
draw.SimpleText("Envoyer une annonce", "bladw_text", w * 0.5, 94, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
draw.SimpleText("Presets", "bladw_text_Medium", 40, 118, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
draw.SimpleText("Destinataires", "bladw_text_Medium", 40, 372, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- bouton fermer
|
||||||
|
local close = vgui.Create("DButton", frame)
|
||||||
|
close:SetSize(30, 30)
|
||||||
|
close:SetPos(frame:GetWide() - 42, 14)
|
||||||
|
close:SetText("")
|
||||||
|
close.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_GRAY, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("X", "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
close.DoClick = function() frame:Remove() end
|
||||||
|
|
||||||
|
-- champs
|
||||||
|
local titleEntry = vgui.Create("DTextEntry", frame)
|
||||||
|
titleEntry:SetPos(40, 240)
|
||||||
|
titleEntry:SetSize(420, 40)
|
||||||
|
StyleEntry(titleEntry, "Titre de l'annonce", "Annonce")
|
||||||
|
|
||||||
|
local msgEntry = vgui.Create("DTextEntry", frame)
|
||||||
|
msgEntry:SetPos(40, 288)
|
||||||
|
msgEntry:SetSize(420, 40)
|
||||||
|
StyleEntry(msgEntry, "Message de l'annonce")
|
||||||
|
msgEntry.Paint = function(self, w, h)
|
||||||
|
local over = EntryLen(self) > maxLen
|
||||||
|
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
local outline = over and COL_RED or (self:IsEditing() and COL_ACCENT or COL_LINE)
|
||||||
|
rndx.DrawOutlined(10, 0, 0, w, h, outline, over and 3 or 2, rndx.SHAPE_FIGMA)
|
||||||
|
self:DrawTextEntryText(COL_TEXT, COL_ACCENT, COL_TEXT)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ligne du bas : warning à gauche, compteur à droite
|
||||||
|
local status = vgui.Create("DPanel", frame)
|
||||||
|
status:SetPos(40, 332)
|
||||||
|
status:SetSize(420, 26)
|
||||||
|
status:SetPaintBackground(false)
|
||||||
|
status.Paint = function(self, w, h)
|
||||||
|
local len = EntryLen(msgEntry)
|
||||||
|
local over = len > maxLen
|
||||||
|
|
||||||
|
if frame.sendFeedback and (frame.sendFeedbackUntil or 0) > SysTime() then
|
||||||
|
DrawWarning(0, h * 0.5 - 8, 16)
|
||||||
|
draw.SimpleText(frame.sendFeedback, "bladw_text_Medium", 24, h * 0.5, COL_WARN, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
elseif over then
|
||||||
|
DrawWarning(0, h * 0.5 - 8, 16)
|
||||||
|
draw.SimpleText("Trop de caractères", "bladw_text_Medium", 24, h * 0.5, COL_RED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
draw.SimpleText(len .. " / " .. maxLen, "bladw_text_Medium", w, h * 0.5,
|
||||||
|
over and COL_RED or COL_MUTED, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- panneau de cases (groupes ou joueurs), hauteur animée
|
||||||
|
local list = vgui.Create("DScrollPanel", frame)
|
||||||
|
list:SetPos(40, LIST_Y)
|
||||||
|
list:SetSize(420, 1)
|
||||||
|
list.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, Color(0, 0, 0, 90), rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
local vbar = list:GetVBar()
|
||||||
|
vbar:SetWide(8)
|
||||||
|
vbar.Paint = function() end
|
||||||
|
vbar.btnUp.Paint = function() end
|
||||||
|
vbar.btnDown.Paint = function() end
|
||||||
|
vbar.btnGrip.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function AddRow(label, isChecked, onToggle)
|
||||||
|
local row = list:Add("DButton")
|
||||||
|
row:Dock(TOP)
|
||||||
|
row:DockMargin(4, 4, 8, 0)
|
||||||
|
row:SetTall(28)
|
||||||
|
row:SetText("")
|
||||||
|
row.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(6, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
local bs, bx = 18, 8
|
||||||
|
local by = (h - bs) * 0.5
|
||||||
|
rndx.DrawOutlined(4, bx, by, bs, bs, isChecked() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||||
|
if isChecked() then
|
||||||
|
rndx.Draw(3, bx + 4, by + 4, bs - 8, bs - 8, COL_ACCENT, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
draw.SimpleText(label, "bladw_text_Medium", bx + bs + 10, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
row.DoClick = function() onToggle() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RebuildList()
|
||||||
|
list:Clear()
|
||||||
|
|
||||||
|
if scope == "group" then
|
||||||
|
local seen = {}
|
||||||
|
for _, p in ipairs(player.GetAll()) do
|
||||||
|
local g = p:GetUserGroup()
|
||||||
|
if g and g ~= "" and not seen[g] then
|
||||||
|
seen[g] = true
|
||||||
|
AddRow(g, function() return selGroups[g] end, function()
|
||||||
|
selGroups[g] = (not selGroups[g]) or nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif scope == "players" then
|
||||||
|
for _, p in ipairs(player.GetAll()) do
|
||||||
|
AddRow(p:Nick(), function() return selPlayers[p] end, function()
|
||||||
|
selPlayers[p] = (not selPlayers[p]) or nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dropdown du scope
|
||||||
|
local scopeBtn = vgui.Create("DButton", frame)
|
||||||
|
scopeBtn:SetPos(40, 392)
|
||||||
|
scopeBtn:SetSize(420, 40)
|
||||||
|
scopeBtn:SetText("")
|
||||||
|
scopeBtn.Paint = function(self, w, h)
|
||||||
|
rndx.Draw(10, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(10, 0, 0, w, h, self:IsHovered() and COL_ACCENT or COL_LINE, 2, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(scopeLabel, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
DrawArrow(w, h)
|
||||||
|
end
|
||||||
|
scopeBtn.DoClick = function(self)
|
||||||
|
local menu = DermaMenu()
|
||||||
|
menu:SetMinimumWidth(self:GetWide())
|
||||||
|
menu.Paint = function(s, w, h)
|
||||||
|
rndx.Draw(8, 0, 0, w, h, Color(18, 18, 18, 252), rndx.SHAPE_FIGMA)
|
||||||
|
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function opt(label, id)
|
||||||
|
local o = menu:AddOption(label, function()
|
||||||
|
scope, scopeLabel = id, label
|
||||||
|
RebuildList()
|
||||||
|
end)
|
||||||
|
o:SetTall(30)
|
||||||
|
o:SetText("")
|
||||||
|
o.Paint = function(s, w, h)
|
||||||
|
if s:IsHovered() then rndx.Draw(6, 3, 1, w - 6, h - 2, COL_ACCENT, rndx.SHAPE_FIGMA) end
|
||||||
|
draw.SimpleText(label, "bladw_text_Medium", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
opt("Tout le monde", "all")
|
||||||
|
opt("Groupe", "group")
|
||||||
|
opt("Joueurs", "players")
|
||||||
|
|
||||||
|
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
|
||||||
|
menu:Open(mx, my)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- bouton envoyer (position gérée par le Think selon l'ouverture)
|
||||||
|
local send = vgui.Create("DButton", frame)
|
||||||
|
send:SetPos(40, LIST_Y)
|
||||||
|
send:SetSize(420, 44)
|
||||||
|
send:SetText("")
|
||||||
|
send.Paint = function(self, w, h)
|
||||||
|
local over = EntryLen(msgEntry) > maxLen
|
||||||
|
local col = over and COL_GRAY or (self:IsHovered() and COL_ACCENT or COL_ACCENT_DIM)
|
||||||
|
rndx.Draw(12, 0, 0, w, h, col, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText("Envoyer", "bladw_text", w * 0.5, h * 0.5, over and COL_MUTED or COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
send.DoClick = function()
|
||||||
|
local function fail(msg)
|
||||||
|
frame.sendFeedback = msg
|
||||||
|
frame.sendFeedbackUntil = SysTime() + 4
|
||||||
|
surface.PlaySound("buttons/button10.wav")
|
||||||
|
end
|
||||||
|
|
||||||
|
if EntryLen(msgEntry) > maxLen then return fail("Trop de caractères") end
|
||||||
|
if msgEntry:GetValue() == "" and selectedPreset == "" then return fail("Message vide") end
|
||||||
|
|
||||||
|
local groups, players = {}, {}
|
||||||
|
if scope == "group" then
|
||||||
|
for g in pairs(selGroups) do groups[#groups + 1] = g end
|
||||||
|
if #groups == 0 then return fail("Choisis au moins un groupe") end
|
||||||
|
elseif scope == "players" then
|
||||||
|
for p in pairs(selPlayers) do if IsValid(p) then players[#players + 1] = p end end
|
||||||
|
if #players == 0 then return fail("Choisis au moins un joueur") end
|
||||||
|
end
|
||||||
|
|
||||||
|
net.Start("bladWSendAnnouncement")
|
||||||
|
net.WriteString(selectedPreset)
|
||||||
|
net.WriteString(titleEntry:GetValue())
|
||||||
|
net.WriteString(msgEntry:GetValue())
|
||||||
|
net.WriteString(scope)
|
||||||
|
if scope == "group" then
|
||||||
|
net.WriteUInt(#groups, 8)
|
||||||
|
for _, g in ipairs(groups) do net.WriteString(g) end
|
||||||
|
elseif scope == "players" then
|
||||||
|
net.WriteUInt(#players, 8)
|
||||||
|
for _, p in ipairs(players) do net.WriteEntity(p) end
|
||||||
|
end
|
||||||
|
net.SendToServer()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ESC ferme le menu (sans menu pause) + animation d'ouverture du panneau
|
||||||
|
frame.Think = function(self)
|
||||||
|
if gui.IsGameUIVisible() then
|
||||||
|
gui.HideGameUI()
|
||||||
|
self:Remove()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target = (scope == "all") and 0 or 1
|
||||||
|
expand = Lerp(FrameTime() * 12, expand, target)
|
||||||
|
if math.abs(expand - target) < 0.002 then expand = target end
|
||||||
|
|
||||||
|
local lh = math.Round(expand * LIST_H)
|
||||||
|
list:SetVisible(lh > 2)
|
||||||
|
list:SetTall(math.max(lh, 1))
|
||||||
|
|
||||||
|
local sy = LIST_Y + lh + (lh > 0 and 12 or 0)
|
||||||
|
send:SetPos(40, sy)
|
||||||
|
self:SetTall(sy + send:GetTall() + 14)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- boutons preset : remplissent les champs, retiennent le preset et règlent les destinataires
|
||||||
|
local presets = vgui.Create("DIconLayout", frame)
|
||||||
|
presets:SetPos(40, 134)
|
||||||
|
presets:SetSize(420, 90)
|
||||||
|
presets:SetSpaceX(8)
|
||||||
|
presets:SetSpaceY(8)
|
||||||
|
|
||||||
|
for key, preset in SortedPairs(BladW.Announcement.Presets or {}) do
|
||||||
|
local b = presets:Add("DButton")
|
||||||
|
b:SetSize(134, 34)
|
||||||
|
b:SetText("")
|
||||||
|
b.Paint = function(self, w, h)
|
||||||
|
local on = (selectedPreset == key) or self:IsHovered()
|
||||||
|
rndx.Draw(8, 0, 0, w, h, on and COL_ACCENT or COL_GRAY, rndx.SHAPE_FIGMA)
|
||||||
|
draw.SimpleText(preset.title or key, "bladw_text_Medium", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||||
|
end
|
||||||
|
b.DoClick = function()
|
||||||
|
selectedPreset = key
|
||||||
|
titleEntry:SetText(preset.title or "")
|
||||||
|
msgEntry:SetText(preset.text or "")
|
||||||
|
|
||||||
|
for k in pairs(selGroups) do selGroups[k] = nil end
|
||||||
|
for k in pairs(selPlayers) do selPlayers[k] = nil end
|
||||||
|
local t = preset.target
|
||||||
|
if not t or t == "" or t == "all" then
|
||||||
|
scope, scopeLabel = "all", "Tout le monde"
|
||||||
|
else
|
||||||
|
scope, scopeLabel = "group", "Groupe"
|
||||||
|
selGroups[t] = true
|
||||||
|
end
|
||||||
|
RebuildList()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RebuildList()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- réponse du serveur après un envoi : publié -> on ferme, refusé -> on garde le menu + on dit pourquoi
|
||||||
|
net.Receive("bladWAnnouncementResult", function()
|
||||||
|
local ok = net.ReadBool()
|
||||||
|
local left = net.ReadUInt(8)
|
||||||
|
|
||||||
|
if not IsValid(BladW_AdminMenu) then return end
|
||||||
|
|
||||||
|
if ok then
|
||||||
|
BladW_AdminMenu:Remove()
|
||||||
|
else
|
||||||
|
BladW_AdminMenu.sendFeedback = left > 0 and ("Annonce déjà en cours (" .. left .. "s)") or "Envoi impossible"
|
||||||
|
BladW_AdminMenu.sendFeedbackUntil = SysTime() + 5
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return OpenAAdminMenu
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
local OpenAAdminMenu = include("bladw_announcement/client/admin_menu/cl_menu.lua")
|
||||||
|
|
||||||
|
|
||||||
|
-- le serveur me dit d'ouvrir le menu
|
||||||
|
net.Receive("bladWOpenAnnouncementMenu", function(len, ply)
|
||||||
|
|
||||||
|
OpenAAdminMenu()
|
||||||
|
|
||||||
|
end)
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
local rndx = include("lib/rndx.lua")
|
||||||
|
local logo = Material("nocode/announcement/logo.png")
|
||||||
|
|
||||||
|
-- client/ charge avant shared/, donc je m'assure que la table est là
|
||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
local COL_ACCENT = Color(76, 119, 200)
|
||||||
|
local COL_WHITE = Color(255, 255, 255)
|
||||||
|
local COL_BG = Color(0, 0, 0, 200)
|
||||||
|
local COL_TRACK = Color(54, 54, 54, 200)
|
||||||
|
|
||||||
|
-- fonts responsives (refaites quand la résolution change)
|
||||||
|
local function AnnScale()
|
||||||
|
return math.min(ScrW() / 1920, ScrH() / 1080)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- durées + easing de l'anim d'apparition/disparition
|
||||||
|
local INTRO_TIME = 0.45
|
||||||
|
local OUTRO_TIME = 0.35
|
||||||
|
local function EaseOutCubic(t) return 1 - (1 - t) ^ 3 end
|
||||||
|
|
||||||
|
local function BuildFonts()
|
||||||
|
local s = AnnScale()
|
||||||
|
surface.CreateFont("bladw_ann_title", {
|
||||||
|
font = "Poppins-SemiBold", size = math.max(1, math.Round(25 * s)), weight = 600, antialias = true,
|
||||||
|
})
|
||||||
|
surface.CreateFont("bladw_ann_text", {
|
||||||
|
font = "Poppins-SemiBold", size = math.max(1, math.Round(20 * s)), weight = 800, antialias = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
BuildFonts()
|
||||||
|
hook.Add("OnScreenSizeChanged", "BladeWin:AnnouncementFonts", BuildFonts)
|
||||||
|
|
||||||
|
-- coupe le texte en lignes qui rentrent dans maxW (px) pour la police donnée
|
||||||
|
local function WrapText(text, font, maxW)
|
||||||
|
surface.SetFont(font)
|
||||||
|
local lines, line = {}, ""
|
||||||
|
|
||||||
|
for _, word in ipairs(string.Explode(" ", text or "")) do
|
||||||
|
local test = (line == "") and word or (line .. " " .. word)
|
||||||
|
if surface.GetTextSize(test) > maxW and line ~= "" then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
line = word
|
||||||
|
else
|
||||||
|
line = test
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if line ~= "" then lines[#lines + 1] = line end
|
||||||
|
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- le contour du cadre sert de jauge : reste en colRemain, ce qui est passé en colDone (1 = plein, 0 = fini)
|
||||||
|
local function DrawBorderTimer(x, y, w, h, r, remaining, colRemain, colDone, thickness)
|
||||||
|
thickness = math.max(1, thickness or 3)
|
||||||
|
|
||||||
|
local pts = {}
|
||||||
|
local function line(x1, y1, x2, y2)
|
||||||
|
local steps = math.max(1, math.floor(math.Distance(x1, y1, x2, y2) / 4))
|
||||||
|
for i = 0, steps do
|
||||||
|
pts[#pts + 1] = { Lerp(i / steps, x1, x2), Lerp(i / steps, y1, y2) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function arc(cx, cy, a0, a1)
|
||||||
|
for i = 0, 6 do
|
||||||
|
local a = math.rad(Lerp(i / 6, a0, a1))
|
||||||
|
pts[#pts + 1] = { cx + math.cos(a) * r, cy + math.sin(a) * r }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- sens horaire à partir du bord haut
|
||||||
|
line(x + r, y, x + w - r, y)
|
||||||
|
arc (x + w - r, y + r, 270, 360)
|
||||||
|
line(x + w, y + r, x + w, y + h - r)
|
||||||
|
arc (x + w - r, y + h - r, 0, 90)
|
||||||
|
line(x + w - r, y + h, x + r, y + h)
|
||||||
|
arc (x + r, y + h - r, 90, 180)
|
||||||
|
line(x, y + h - r, x, y + r)
|
||||||
|
arc (x + r, y + r, 180, 270)
|
||||||
|
|
||||||
|
local total = 0
|
||||||
|
for i = 1, #pts - 1 do
|
||||||
|
total = total + math.Distance(pts[i][1], pts[i][2], pts[i + 1][1], pts[i + 1][2])
|
||||||
|
end
|
||||||
|
if total == 0 then return end
|
||||||
|
|
||||||
|
local consumedLen = (1 - remaining) * total
|
||||||
|
draw.NoTexture()
|
||||||
|
|
||||||
|
local acc = 0
|
||||||
|
for i = 1, #pts - 1 do
|
||||||
|
local x1, y1 = pts[i][1], pts[i][2]
|
||||||
|
local x2, y2 = pts[i + 1][1], pts[i + 1][2]
|
||||||
|
local segLen = math.Distance(x1, y1, x2, y2)
|
||||||
|
|
||||||
|
surface.SetDrawColor(acc < consumedLen and colDone or colRemain)
|
||||||
|
surface.DrawTexturedRectRotated((x1 + x2) * 0.5, (y1 + y2) * 0.5,
|
||||||
|
segLen + thickness, thickness, -math.deg(math.atan2(y2 - y1, x2 - x1)))
|
||||||
|
|
||||||
|
acc = acc + segLen
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- état de l'annonce en cours (survit au reload)
|
||||||
|
BladW.Announcement.Active = BladW.Announcement.Active or false
|
||||||
|
BladW.Announcement.Title = BladW.Announcement.Title or "Annonce"
|
||||||
|
BladW.Announcement.Text = BladW.Announcement.Text or ""
|
||||||
|
BladW.Announcement.Style = BladW.Announcement.Style or "loader"
|
||||||
|
BladW.Announcement.Duration = BladW.Announcement.Duration or 10
|
||||||
|
BladW.Announcement.StartTime = BladW.Announcement.StartTime or 0
|
||||||
|
|
||||||
|
-- lance une annonce. style/duration vides = valeurs de la config
|
||||||
|
function BladW.Announcement.Show(title, text, style, duration)
|
||||||
|
local Conf = BladW.Announcement.Conf or {}
|
||||||
|
|
||||||
|
BladW.Announcement.Title = (title and title ~= "") and title or "Annonce"
|
||||||
|
BladW.Announcement.Text = text or ""
|
||||||
|
BladW.Announcement.Style = (style and style ~= "") and style or (Conf.announcement_style or "loader")
|
||||||
|
BladW.Announcement.Duration = (duration and duration > 0) and duration or (Conf.announcement_duration or 10)
|
||||||
|
BladW.Announcement.StartTime = CurTime()
|
||||||
|
BladW.Announcement.Active = true
|
||||||
|
|
||||||
|
if Conf.announcement_sound and Conf.announcement_sound ~= "" then
|
||||||
|
surface.PlaySound(Conf.announcement_sound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- le serveur m'envoie une annonce à afficher
|
||||||
|
net.Receive("bladWShowAnnouncement", function()
|
||||||
|
local title = net.ReadString()
|
||||||
|
local text = net.ReadString()
|
||||||
|
local style = net.ReadString()
|
||||||
|
local duration = net.ReadUInt(16)
|
||||||
|
BladW.Announcement.Show(title, text, style, duration)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- affichage (responsive + s'adapte au message)
|
||||||
|
hook.Add("HUDPaint", "BladeWin:Announcement", function()
|
||||||
|
if not BladW.Announcement.Active then return end
|
||||||
|
|
||||||
|
local ply = LocalPlayer()
|
||||||
|
if not IsValid(ply) then return end
|
||||||
|
|
||||||
|
local duration = BladW.Announcement.Duration or 10
|
||||||
|
local elapsed = CurTime() - BladW.Announcement.StartTime
|
||||||
|
|
||||||
|
-- temps écoulé -> on cache
|
||||||
|
if elapsed >= duration then
|
||||||
|
BladW.Announcement.Active = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local style = BladW.Announcement.Style or "loader"
|
||||||
|
local title = BladW.Announcement.Title
|
||||||
|
local text = BladW.Announcement.Text
|
||||||
|
local remaining = math.Clamp(1 - (elapsed / duration), 0, 1)
|
||||||
|
|
||||||
|
-- échelle responsive
|
||||||
|
local s = AnnScale()
|
||||||
|
local padX = 26 * s
|
||||||
|
local padY = 18 * s
|
||||||
|
local gap = 8 * s
|
||||||
|
local logoS = 62 * s
|
||||||
|
local barW = 200 * s
|
||||||
|
local barH = 18 * s
|
||||||
|
local radius = 14 * s
|
||||||
|
|
||||||
|
-- wrap du message (largeur interne responsive, la vraie limite c'est le nb de caractères)
|
||||||
|
local wrapW = ScrW() * 0.8
|
||||||
|
local lines = WrapText(text, "bladw_ann_text", wrapW)
|
||||||
|
|
||||||
|
surface.SetFont("bladw_ann_title")
|
||||||
|
local titleW, titleH = surface.GetTextSize(title)
|
||||||
|
|
||||||
|
surface.SetFont("bladw_ann_text")
|
||||||
|
local _, lineH = surface.GetTextSize("Ay")
|
||||||
|
local msgW = 0
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
msgW = math.max(msgW, (surface.GetTextSize(l)))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- taille de la box selon le contenu
|
||||||
|
local contentW = math.max(titleW, msgW, style == "loader" and barW or 0)
|
||||||
|
local boxW = math.Clamp(contentW + padX * 2, 260 * s, ScrW() * 0.92)
|
||||||
|
local msgH = #lines * lineH
|
||||||
|
local boxH = padY + logoS + gap + titleH + gap + msgH
|
||||||
|
+ (style == "loader" and (gap + barH) or 0) + padY
|
||||||
|
|
||||||
|
-- anim : fondu + glisse du haut (et l'inverse quand ça part)
|
||||||
|
local appear = math.Clamp(elapsed / INTRO_TIME, 0, 1)
|
||||||
|
local disappear = math.Clamp((duration - elapsed) / OUTRO_TIME, 0, 1)
|
||||||
|
local anim = math.min(EaseOutCubic(appear), EaseOutCubic(disappear)) -- 0 -> 1 -> 0
|
||||||
|
local slide = (1 - anim) * 26 * s
|
||||||
|
|
||||||
|
local boxX = (ScrW() - boxW) * 0.5
|
||||||
|
local boxY = ScrH() * 0.035 - slide
|
||||||
|
local cx = boxX + boxW * 0.5
|
||||||
|
|
||||||
|
surface.SetAlphaMultiplier(anim)
|
||||||
|
|
||||||
|
-- fond
|
||||||
|
rndx.Draw(radius, boxX, boxY, boxW, boxH, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
|
||||||
|
rndx.Draw(radius, boxX, boxY, boxW, boxH, COL_BG, rndx.SHAPE_FIGMA)
|
||||||
|
|
||||||
|
-- le loader garde son contour blanc (le simple lui a sa jauge sur le bord)
|
||||||
|
if style == "loader" then
|
||||||
|
rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, COL_WHITE, 3 * s, rndx.SHAPE_FIGMA)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- contenu empilé et centré
|
||||||
|
local y = boxY + padY
|
||||||
|
rndx.DrawMaterial(0, cx - logoS * 0.5, y, logoS, logoS, COL_WHITE, logo)
|
||||||
|
y = y + logoS + gap
|
||||||
|
|
||||||
|
draw.SimpleText(title, "bladw_ann_title", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
|
||||||
|
y = y + titleH + gap
|
||||||
|
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
draw.SimpleText(l, "bladw_ann_text", cx, y, COL_WHITE, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
|
||||||
|
y = y + lineH
|
||||||
|
end
|
||||||
|
|
||||||
|
-- jauge de temps
|
||||||
|
if style == "loader" then
|
||||||
|
y = y + gap
|
||||||
|
local bx = cx - barW * 0.5
|
||||||
|
rndx.Draw(barH * 0.5, bx, y, barW, barH, COL_TRACK, rndx.SHAPE_FIGMA)
|
||||||
|
rndx.Draw(barH * 0.5, bx, y, barW * remaining, barH, COL_ACCENT, rndx.SHAPE_FIGMA)
|
||||||
|
else
|
||||||
|
-- le bord bleu qui se vide en blanc
|
||||||
|
DrawBorderTimer(boxX, boxY, boxW, boxH, radius, remaining, COL_ACCENT, COL_WHITE, 3 * s)
|
||||||
|
end
|
||||||
|
|
||||||
|
surface.SetAlphaMultiplier(1)
|
||||||
|
end)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
-- pour que les clients téléchargent le son
|
||||||
|
resource.AddFile("sound/announce.mp3")
|
||||||
|
|
||||||
|
-- coupe une chaîne à maxChars caractères (propre avec les accents)
|
||||||
|
local function LimitChars(str, maxChars)
|
||||||
|
str = tostring(str or "")
|
||||||
|
local n = utf8.len(str)
|
||||||
|
if not n or n <= maxChars then return str end
|
||||||
|
local endByte = utf8.offset(str, maxChars + 1)
|
||||||
|
return endByte and string.sub(str, 1, endByte - 1) or str
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ouvre le menu pour le joueur (faut avoir le droit)
|
||||||
|
concommand.Add("bladw_amenu", function(ply, cmd)
|
||||||
|
if not IsValid(ply) or not BladW.Announcement.HasAccess(ply) then return end
|
||||||
|
|
||||||
|
net.Start("bladWOpenAnnouncementMenu")
|
||||||
|
net.Send(ply)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- /bannonce (ou !bannonce) dans le chat = ouvre le menu
|
||||||
|
hook.Add("PlayerSay", "BladW:AnnouncementChatCmd", function(ply, text)
|
||||||
|
local cmd = string.lower(string.Trim(text))
|
||||||
|
if cmd ~= "/bannonce" and cmd ~= "!bannonce" then return end
|
||||||
|
|
||||||
|
-- que ceux qui ont le droit ouvrent le menu, mais on cache la commande pour tout le monde
|
||||||
|
if IsValid(ply) and BladW.Announcement.HasAccess(ply) then
|
||||||
|
net.Start("bladWOpenAnnouncementMenu")
|
||||||
|
net.Send(ply)
|
||||||
|
end
|
||||||
|
|
||||||
|
return "" -- commande reconnue -> on la retire du chat
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- reconstruit l'annonce finale : le preset sert de base, title/text passent devant
|
||||||
|
function BladW.Announcement.Resolve(entry)
|
||||||
|
local presets = BladW.Announcement.Presets or {}
|
||||||
|
local base = (entry.preset and presets[entry.preset]) or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title = (entry.title and entry.title ~= "" and entry.title) or base.title or "Annonce",
|
||||||
|
text = (entry.text and entry.text ~= "" and entry.text ) or base.text or "",
|
||||||
|
style = entry.style or base.style or "",
|
||||||
|
duration = tonumber(entry.duration) or tonumber(base.duration) or 0,
|
||||||
|
target = entry.target or base.target, -- usergroup ciblé (nil = tout le monde)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- envoie l'annonce à tout le monde. false si rien à envoyer ou si une annonce est déjà en cours
|
||||||
|
function BladW.Announcement.Broadcast(entry)
|
||||||
|
-- déjà une annonce à l'écran ? on n'en empile pas une autre
|
||||||
|
if CurTime() < (BladW.Announcement.activeUntil or 0) then return false end
|
||||||
|
|
||||||
|
local a = BladW.Announcement.Resolve(entry)
|
||||||
|
if a.text == "" then return false end
|
||||||
|
|
||||||
|
-- destinataires (nil = tout le monde) :
|
||||||
|
-- liste de joueurs (menu) > liste de groupes (menu) + target unique (preset/schedule)
|
||||||
|
local recipients
|
||||||
|
if istable(entry.players) and #entry.players > 0 then
|
||||||
|
recipients = {}
|
||||||
|
for _, p in ipairs(entry.players) do
|
||||||
|
if IsValid(p) and p:IsPlayer() then recipients[#recipients + 1] = p end
|
||||||
|
end
|
||||||
|
elseif (istable(entry.groups) and #entry.groups > 0)
|
||||||
|
or (a.target and a.target ~= "" and a.target ~= "all") then
|
||||||
|
local set = {}
|
||||||
|
if istable(entry.groups) then
|
||||||
|
for _, g in ipairs(entry.groups) do set[g] = true end
|
||||||
|
end
|
||||||
|
if a.target and a.target ~= "" and a.target ~= "all" then set[a.target] = true end
|
||||||
|
|
||||||
|
recipients = {}
|
||||||
|
for _, p in ipairs(player.GetAll()) do
|
||||||
|
if set[p:GetUserGroup()] then recipients[#recipients + 1] = p end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if recipients and #recipients == 0 then return false end -- personne à qui envoyer
|
||||||
|
|
||||||
|
local Conf = BladW.Announcement.Conf or {}
|
||||||
|
local maxLen = tonumber(Conf.announcement_max_length) or 120
|
||||||
|
local dur = a.duration > 0 and a.duration or (tonumber(Conf.announcement_duration) or 10)
|
||||||
|
|
||||||
|
-- on bloque les suivantes le temps que celle-ci s'affiche
|
||||||
|
BladW.Announcement.activeUntil = CurTime() + dur
|
||||||
|
|
||||||
|
net.Start("bladWShowAnnouncement")
|
||||||
|
net.WriteString(LimitChars(a.title, 64))
|
||||||
|
net.WriteString(LimitChars(a.text, maxLen))
|
||||||
|
net.WriteString(a.style or "")
|
||||||
|
net.WriteUInt(math.Clamp(a.duration, 0, 65535), 16)
|
||||||
|
if recipients then net.Send(recipients) else net.Broadcast() end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- un admin veut poster une annonce -> je la diffuse
|
||||||
|
net.Receive("bladWSendAnnouncement", function(len, ply)
|
||||||
|
if not IsValid(ply) or not BladW.Announcement.HasAccess(ply) then return end
|
||||||
|
|
||||||
|
local preset = net.ReadString()
|
||||||
|
local title = net.ReadString()
|
||||||
|
local text = net.ReadString()
|
||||||
|
|
||||||
|
-- destinataires : "all" / "group" (liste de groupes) / "players" (liste de joueurs)
|
||||||
|
-- le choix du menu est explicite -> il prime sur le target du preset
|
||||||
|
local scope = net.ReadString()
|
||||||
|
local target, groups, players = "all", nil, nil
|
||||||
|
if scope == "group" then
|
||||||
|
target = "" -- vide = on ignore le target du preset, on prend la liste
|
||||||
|
groups = {}
|
||||||
|
for _ = 1, net.ReadUInt(8) do groups[#groups + 1] = net.ReadString() end
|
||||||
|
elseif scope == "players" then
|
||||||
|
target = ""
|
||||||
|
players = {}
|
||||||
|
for _ = 1, net.ReadUInt(8) do
|
||||||
|
local p = net.ReadEntity()
|
||||||
|
if IsValid(p) and p:IsPlayer() then players[#players + 1] = p end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok = BladW.Announcement.Broadcast({
|
||||||
|
preset = preset ~= "" and preset or nil,
|
||||||
|
title = title,
|
||||||
|
text = text,
|
||||||
|
target = target,
|
||||||
|
groups = groups,
|
||||||
|
players = players,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- je réponds à l'admin : publié -> il ferme le menu, refusé -> il garde son texte
|
||||||
|
local left = math.Clamp(math.ceil((BladW.Announcement.activeUntil or 0) - CurTime()), 0, 255)
|
||||||
|
net.Start("bladWAnnouncementResult")
|
||||||
|
net.WriteBool(ok)
|
||||||
|
net.WriteUInt(left, 8)
|
||||||
|
net.Send(ply)
|
||||||
|
end)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
util.AddNetworkString("bladWOpenAnnouncementMenu") -- ouvre le menu chez le client
|
||||||
|
util.AddNetworkString("bladWSendAnnouncement") -- l'admin m'envoie son annonce
|
||||||
|
util.AddNetworkString("bladWShowAnnouncement") -- je balance l'annonce à tout le monde
|
||||||
|
util.AddNetworkString("bladWAnnouncementResult") -- je dis à l'admin si son annonce est passée
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
-- le droit géré par l'admin mod (SAM / ULX / FAdmin)
|
||||||
|
local PRIV = "bladw_announce"
|
||||||
|
BladW.Announcement.Privilege = PRIV
|
||||||
|
|
||||||
|
-- j'enregistre le droit dans l'admin mod présent, pour qu'il apparaisse dans son menu
|
||||||
|
local function Register()
|
||||||
|
-- SAM
|
||||||
|
if sam and sam.permissions and sam.permissions.add then
|
||||||
|
pcall(sam.permissions.add, PRIV, "BladW Annonce", "admin")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ULX / ULib
|
||||||
|
if ULib and ULib.ucl and ULib.ucl.registerAccess then
|
||||||
|
pcall(ULib.ucl.registerAccess, PRIV, "admin", "Gérer les annonces BladW", "BladW")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- FAdmin (DarkRP) : 2 = admin
|
||||||
|
if FAdmin and FAdmin.Access and FAdmin.Access.AddPrivilege then
|
||||||
|
pcall(FAdmin.Access.AddPrivilege, PRIV, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- SAM veut qu'on (ré)enregistre ses permissions sur ce hook
|
||||||
|
hook.Add("SAM.LoadPermissions", "BladW:AnnouncePriv", Register)
|
||||||
|
-- les autres mods sont parfois chargés après le serveur -> on retente
|
||||||
|
hook.Add("InitPostEntity", "BladW:AnnouncePriv", Register)
|
||||||
|
-- et un coup direct au cas où le mod est déjà là (hot-reload)
|
||||||
|
Register()
|
||||||
|
|
||||||
|
-- est-ce que ce joueur a le droit de gérer les annonces ?
|
||||||
|
function BladW.Announcement.HasAccess(ply)
|
||||||
|
if not IsValid(ply) then return false end
|
||||||
|
|
||||||
|
-- SAM
|
||||||
|
if sam and sam.player and sam.player.has_permission then
|
||||||
|
return sam.player.has_permission(ply, PRIV)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ULX / ULib
|
||||||
|
if ULib and ULib.ucl and ULib.ucl.query then
|
||||||
|
return ULib.ucl.query(ply, PRIV)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- FAdmin (DarkRP)
|
||||||
|
if FAdmin and FAdmin.Access and FAdmin.Access.PlayerHasPrivilege then
|
||||||
|
return FAdmin.Access.PlayerHasPrivilege(ply, PRIV)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- pas d'admin mod -> on retombe sur l'admin de base
|
||||||
|
return ply:IsAdmin()
|
||||||
|
end
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
BladW.Announcement.Conf = BladW.Announcement.Conf or {}
|
||||||
|
|
||||||
|
BladW.Announcement.Conf = {
|
||||||
|
announcement_style = "simple", -- "simple" ou "loader"
|
||||||
|
announcement_duration = 10, -- durée d'affichage en secondes
|
||||||
|
announcement_max_length = 80, -- max de caractères pour le message
|
||||||
|
announcement_sound = "announce.mp3", -- son à l'apparition ("" = rien), fichier dans sound/ | ex : garrysmod/content_downloaded.wav
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
-- mes presets d'annonce (réutilisables dans le menu et par le planificateur)
|
||||||
|
-- title / text, et en option style ("simple"/"loader") + duration (sinon valeurs de sh_config)
|
||||||
|
-- target : à qui c'est envoyé -> "all" (tout le monde) ou un usergroup ("vip", "admin"...)
|
||||||
|
-- la clé (restart, shutdown...) sert d'identifiant dans sh_schedule.lua
|
||||||
|
BladW.Announcement.Presets = {
|
||||||
|
|
||||||
|
restart = {
|
||||||
|
title = "Redémarrage",
|
||||||
|
text = "Le serveur va redémarrer dans quelques instants. Reconnexion sous peu !",
|
||||||
|
style = "loader",
|
||||||
|
duration = 15,
|
||||||
|
target = "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
shutdown = {
|
||||||
|
title = "Arrêt du serveur",
|
||||||
|
text = "Le serveur va s'arrêter. Merci d'avoir joué avec nous !",
|
||||||
|
style = "loader",
|
||||||
|
duration = 15,
|
||||||
|
target = "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
maintenance = {
|
||||||
|
title = "Maintenance",
|
||||||
|
text = "Une maintenance va débuter, le serveur peut lag ou redémarrer.",
|
||||||
|
style = "simple",
|
||||||
|
duration = 12,
|
||||||
|
target = "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
event = {
|
||||||
|
title = "Événement",
|
||||||
|
text = "Un événement démarre bientôt, rejoignez-nous !",
|
||||||
|
style = "loader",
|
||||||
|
duration = 12,
|
||||||
|
target = "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
discord = {
|
||||||
|
title = "Discord",
|
||||||
|
text = "Rejoignez notre Discord pour rester informé des nouveautés !",
|
||||||
|
style = "simple",
|
||||||
|
duration = 10,
|
||||||
|
target = "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
BladW = BladW or {}
|
||||||
|
BladW.Announcement = BladW.Announcement or {}
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────
|
||||||
|
-- Annonces automatiques (mon petit cron maison)
|
||||||
|
-- Chaque ligne = une annonce qui part toute seule.
|
||||||
|
--
|
||||||
|
-- QUAND ? une seule des deux clés :
|
||||||
|
--
|
||||||
|
-- every = "30m" -> ça répète tous les X. Unités : s (sec), m (min), h (heure), d (jour)
|
||||||
|
-- ex : "45s", "10m", "2h", "1d"
|
||||||
|
--
|
||||||
|
-- cron = "0 3 * * *" -> comme sur Linux, 5 champs séparés par un espace :
|
||||||
|
-- 1) minute 0-59
|
||||||
|
-- 2) heure 0-23
|
||||||
|
-- 3) jour du mois 1-31
|
||||||
|
-- 4) mois 1-12
|
||||||
|
-- 5) jour semaine 0-6 (0 = dimanche)
|
||||||
|
--
|
||||||
|
-- dans chaque champ je peux mettre :
|
||||||
|
-- * -> tout le temps
|
||||||
|
-- */5 -> tous les 5 (0, 5, 10...)
|
||||||
|
-- 1-5 -> une plage (de 1 à 5)
|
||||||
|
-- 1,3,5 -> une liste
|
||||||
|
--
|
||||||
|
-- quelques exemples qui parlent :
|
||||||
|
-- "*/15 * * * *" -> toutes les 15 min
|
||||||
|
-- "0 * * * *" -> à chaque heure pile
|
||||||
|
-- "0 */2 * * *" -> toutes les 2 heures
|
||||||
|
-- "0 20 * * 5" -> tous les vendredis à 20h
|
||||||
|
-- "30 4 1 * *" -> le 1er du mois à 4h30
|
||||||
|
-- (c'est l'heure du serveur qui compte)
|
||||||
|
--
|
||||||
|
-- QUOI ? soit un preset, soit du sur-mesure :
|
||||||
|
-- preset = "restart" -> reprend un preset (voir sh_presets.lua)
|
||||||
|
-- title = "...", text = "..." -> annonce perso
|
||||||
|
-- (si je mets les deux, title/text passent devant le preset)
|
||||||
|
--
|
||||||
|
-- À QUI ? en option :
|
||||||
|
-- target = "vip" -> seulement l'usergroup "vip" (sinon tout le monde)
|
||||||
|
--
|
||||||
|
-- Pour couper une ligne : je la commente (--) ou je la dégage.
|
||||||
|
-- ─────────────────────────────────────────────
|
||||||
|
BladW.Announcement.Schedules = {
|
||||||
|
|
||||||
|
-- rappel Discord toutes les 30 min
|
||||||
|
{ every = "30m", preset = "discord" },
|
||||||
|
|
||||||
|
-- petite astuce à chaque heure pile
|
||||||
|
{ cron = "0 * * * *", title = "Astuce", text = "Tapez /bannonce pour gérer les annonces." },
|
||||||
|
|
||||||
|
-- exemples à décommenter au besoin
|
||||||
|
-- { cron = "0 5 * * *", preset = "restart" }, -- reboot tous les jours à 5h
|
||||||
|
-- { cron = "0 20 * * 5", preset = "event" }, -- event le vendredi à 20h
|
||||||
|
-- { every = "2h", preset = "discord" }, -- rappel Discord toutes les 2h
|
||||||
|
|
||||||
|
}
|
||||||
+12
-2
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
Binary file not shown.
Reference in New Issue
Block a user